保存数据

CakePHP 使保存模型数据易如反掌。准备保存的数据应当以下面的基本格式传递给模型的save() 方法:

  1. Array
  2. (
  3. [ModelName] => Array
  4. (
  5. [fieldname1] => 'value'
  6. [fieldname2] => 'value'
  7. )
  8. )

大多数时候你根本无需担心这种格式:CakePHP 的 FormHelper 和模型的find 方法都会将数据包装成这种格式。如果使用 FormHelper,数据也在$this->request->data 方便地可供立即使用。

下面是控制器动作使用 CakePHP 模型向数据库表存入数据的简单示例:

  1. public function edit($id) {
  2. // 是否有表单数据被提交(*POSTed*)?
  3. if ($this->request->is('post')) {
  4. // 如果表单数据能够通过验证并保存成功……
  5. if ($this->Recipe->save($this->request->data)) {
  6. // 设置会话(*session*)闪现提示信息并跳转
  7. $this->Session->setFlash('Recipe Saved!');
  8. return $this->redirect('/recipes');
  9. }
  10. }
  11.  
  12. // 如果没有表单数据,查找要编辑的菜单(*recipe*)并将其交给视图
  13. $this->set('recipe', $this->Recipe->findById($id));
  14. }

当调用 save 方法时,在第一个参数中传入的数据会使用 CakePHP 的验证机制进行验证(欲知详情,请参见 数据验证 一章)。如果因为某些原因,数据没有保存,一定要检查是否是某些验证规则没有通过。这种情况下,可以通过输出Model::$validationErrors 来进行调试:

  1. if ($this->Recipe->save($this->request->data)) {
  2. // 处理成功的情况。
  3. }
  4. debug($this->Recipe->validationErrors);

模型中还有其它一些与保存相关的方法会对你有用:

Model::set($one, $two = null)

Model::set() 可以用于将一个或多个字段的数据设置到模型的 data 数组中。这可用于把模型和模型提供的 ActiveRecord 特性一起使用:

  1. $this->Post->read(null, 1);
  2. $this->Post->set('title', 'New title for the article');
  3. $this->Post->save();

此例展示了如何以 ActiveRecord 的方式,使用 set() 方法更新单个列。也可以使用set() 给多个字段赋予新值:

  1. $this->Post->read(null, 1);
  2. $this->Post->set(array(
  3. 'title' => 'New title',
  4. 'published' => false
  5. ));
  6. $this->Post->save();

上例会更新 title 和 published 字段,并把记录保存到数据库中。

Model::clear()

该方法可用于重置模型状态,并清除任何未保存数据及验证错误。

2.4 新版功能.

Model::save(array $data = null, boolean $validate = true, array $fieldList = array())

如前所示,这个方法保存数组格式的数据。第二个参数让你可以跳过验证,第三个参数让你可以提供要保存的模型字段列表。为了增强安全性,可以使用 $fieldList 限制要保存的字段。

注解

如果不提供 $fieldList,恶意的用户能够向表单数据中添加额外的字段(在你没有使用 SecurityComponent 的情况下),从而改变原本不希望被改变的字段。

save 方法还有另外一种语法:

  1. save(array $data = null, array $params = array())

$params 数组可以使用如下任意选项作为其键:

  • validate 设置为 true/false 来开启/关闭验证。
  • fieldList 允许保存的字段数组。
  • callbacks 设置为 false 将禁止回调。使用 'before' 或 'after' 将仅开启指定的回调。
  • counterCache (从 2.4 版本开始)控制计数器缓存(如果有的话)更新的布尔值
  • atomic (从 2.6 版本开始) 指定要使用事务保存记录的布尔值。
    欲知模型回调的更多信息,请参见 这里

小技巧

如果你不希望保存某些数据时自动更新 modified 字段,在 $data 数组中添加'modified' => false

一旦保存完成,可以使用模型对象的 $id 属性获得对象的 ID —— 在创建新对象时可能会非常方便。

  1. $this->Ingredient->save($newData);
  2. $newIngredientId = $this->Ingredient->id;

创建或更新是通过模型的 id 字段来控制的。如果设置了 $Model->id,带有这个主键的记录将被更新,否则将创建新记录:

  1. // 建新记录:id 没有设置或为 null
  2. $this->Recipe->create();
  3. $this->Recipe->save($this->request->data);
  4.  
  5. // 更新记录: id 被设置为一个数值
  6. $this->Recipe->id = 2;
  7. $this->Recipe->save($this->request->data);

小技巧

在循环中调用 save 方法时,不要忘记调用 clear()

如果想更新一条记录,而不是创建一条新记录,请确保向数据数组传入了主键字段:

  1. $data = array('id' => 10, 'title' => 'My new title');
  2. // 会更新 id 为 10 的 Recipe 记录
  3. $this->Recipe->save($data);

Model::create(array $data = array())

这个方法为保存新数据重置模型的状态。实际上它并不在数据库中创建新记录,而是清除Model::$id,并按照数据库字段的默认值设置 Model::$data。如果没有定义数据库字段的默认值,Model::$data 会被设置空数组。

如果传入了 $data 参数(使用上面描述的数组格式),这会和数据库字段的默认值合并,并准备好模型实例来保存这些数据(可由 $this->data 访问)。

如果传入 falsenull$data 参数,Model::$data 会被设置为空数组。

小技巧

如果要插入一新行而不是更新已存在的一行,应当总是先调用 create()。这样能够在回调函数或者其它地方避免与之前的 save 调用发生冲突。

Model::saveField(string $fieldName, string $fieldValue, $validate = false)

用于保存单个字段的值。在将要调用 saveField() 之前要设置模型的 ID ($this->ModelName->id = $id)。在使用该方法时,$fieldName 应当只包含字段名,而不是模型名和字段名。

例如,更新一篇博客文章(blog post)的标题(title),在控制器中调用 saveField方法可以象下面这样:

  1. $this->Post->saveField('title', 'A New Title for a New Day');

警告

在使用这个方法时不能阻止更新 modified 字段,你需要使用 save() 方法才行。

saveField 方法也有另一种语法:

  1. saveField(string $fieldName, string $fieldValue, array $params = array())

$params 数组可以用如下任意选项作为键:

  • validate 设置为 true/false 来开启/关闭验证。
  • callbacks 设置为 false 来关闭回调。使用 'before' 或 'after' 将仅开启指定的回调。
  • counterCache (从 2.4 版本开始)控制计数器缓存(如果有的话)更新的布尔值

Model::updateAll(array $fields, mixed $conditions)

用一次调用更新一条或多条记录。要更新的字段和它们的值,由 $fields 数组确定。要更新的记录由 $conditions 数组确定。如果 $conditions 参数未提供或设置为true,全部记录都会被更新。

例如,批准所有成为会员超过一年的 bakers,更新调用可以象下面这样:

  1. $thisYear = date('Y-m-d H:i:s', strtotime('-1 year'));
  2.  
  3. $this->Baker->updateAll(
  4. array('Baker.approved' => true),
  5. array('Baker.created <=' => $thisYear)
  6. );

$fields 数组可接受 SQL 表达式。常量(literal)值应当使用DboSource::value() 手动引用。例如,如果一个模型方法调用updateAll(),应该这样:

  1. $db = $this->getDataSource();
  2. $value = $db->value($value, 'string');
  3. $this->updateAll(
  4. array('Baker.status' => $value),
  5. array('Baker.status' => 'old')
  6. );

注解

即使更新的模型中有 modified 字段,它也不会被 ORM 自动更新。如果要更新它,只需手动将其加入数组中。

例如,关闭所有属于某一客户的所有请求:

  1. $this->Ticket->updateAll(
  2. array('Ticket.status' => "'closed'"),
  3. array('Ticket.customer_id' => 453)
  4. );

默认情况下,updateAll() 对支持 join 的数据库会自动连接任何 belongsTo 关联。要阻止这种连接,临时解除绑定(unbind)该关联。

Model::saveMany(array $data = null, array $options = array())

此方法用于同时保存同一模型的多行。可以使用如下选项:

  • validate: 设置为 false 将关闭验证,设置为 true 将在保存每条记录前进行验证,设置为 'first' 将在保存任何记录前验证 所有 记录(默认值)
  • atomic: 如果为 true (默认值),将试图用单个事务保存所有记录。如果数据库/表不支持事务,则应当设置为 false。
  • fieldList: 同 Model::save() 方法的 $fieldList 参数
  • deep: (从 2.1 版开始) 如果设置为 true,关联数据也被保存;也可参见saveAssociated 方法。
  • callbacks 设置为 false 将关闭回调。使用 'before' 或 'after' 将仅开启指定的回调。
  • counterCache (从 2.4 版本开始)控制计数器缓存(如果有的话)更新的布尔值
    为保存单个模型的多条记录,$data 必须是数字索引的记录数组,象这样:
  1. $data = array(
  2. array('title' => 'title 1'),
  3. array('title' => 'title 2'),
  4. );

注解

注意,我们传递了数字索引、而非通常情况下 $data 包含的 Article 键。在保存同一模型的多条记录时,记录数组应当只使用数字索引,而不是模型的键。

也可以使用如下格式的数据:

  1. $data = array(
  2. array('Article' => array('title' => 'title 1')),
  3. array('Article' => array('title' => 'title 2')),
  4. );

如果要使用 $options['deep'] = true (从 2.1 版本开始)一起保存关联数据,上面的两个例子将象下面这样:

  1. $data = array(
  2. array('title' => 'title 1', 'Assoc' => array('field' => 'value')),
  3. array('title' => 'title 2'),
  4. );
  5. $data = array(
  6. array(
  7. 'Article' => array('title' => 'title 1'),
  8. 'Assoc' => array('field' => 'value')
  9. ),
  10. array('Article' => array('title' => 'title 2')),
  11. );
  12. $Model->saveMany($data, array('deep' => true));

切记,如果只想更新记录而不是创建新记录,只需要在数据行中加入主键索引:

  1. $data = array(
  2. array(
  3. // 这会创建一个新行
  4. 'Article' => array('title' => 'New article')),
  5. array(
  6. // 这会更新一个现有的行
  7. 'Article' => array('id' => 2, 'title' => 'title 2')),
  8. );

Model::saveAssociated(array $data = null, array $options = array())

此方法用于同时保存多个模型关联。可以使用如下选项:

  • validate: 设置为 false 将关闭验证,设置为 true 将在保存每条记录前进行验证,设置为 'first' 将在保存任何记录前验证 全部 记录(默认值)
  • atomic: 如果为 true (默认值),将试图用单个事务保存所有记录。如果数据库/表不支持事务,则应当设置为 false。
  • fieldList: 同 Model::save() 方法的 $fieldList 参数
  • deep: (从 2.1 版开始) 如果设置为 true,不仅保存直接相关的关联数据,也会保存深度嵌套的关联数据。默认值为 false。
  • counterCache (从 2.4 版本开始)控制计数器缓存(如果有的话)更新的布尔值
    要同时保存一条记录及与其有着 hasOne 或者 belongsTo 关联的相关记录,data 数组应当象下面这样:
  1. $data = array(
  2. 'User' => array('username' => 'billy'),
  3. 'Profile' => array('sex' => 'Male', 'occupation' => 'Programmer'),
  4. );

要同时保存一条记录及与其有着 hasMany 关联的相关记录,data 数组应当象下面这样:

  1. $data = array(
  2. 'Article' => array('title' => 'My first article'),
  3. 'Comment' => array(
  4. array('body' => 'Comment 1', 'user_id' => 1),
  5. array('body' => 'Comment 2', 'user_id' => 12),
  6. array('body' => 'Comment 3', 'user_id' => 40),
  7. ),
  8. );

而要同时保存一条记录及与其有着超过两层深度的 hasMany 关联的相关记录,data 数组应当象下面这样:

  1. $data = array(
  2. 'User' => array('email' => 'john-doe@cakephp.org'),
  3. 'Cart' => array(
  4. array(
  5. 'payment_status_id' => 2,
  6. 'total_cost' => 250,
  7. 'CartItem' => array(
  8. array(
  9. 'cart_product_id' => 3,
  10. 'quantity' => 1,
  11. 'cost' => 100,
  12. ),
  13. array(
  14. 'cart_product_id' => 5,
  15. 'quantity' => 1,
  16. 'cost' => 150,
  17. )
  18. )
  19. )
  20. )
  21. );

注解

如果保存成功,主模型的外键将被存储在相关模型的 id 字段中,即$this->RelatedModel->id

为了同时保存一条记录及与其有 hasMany 关联的相关记录、以及深层关联的 CommentbelongsTo User 数据,data 数组应当象这样:

  1. $data = array(
  2. 'Article' => array('title' => 'My first article'),
  3. 'Comment' => array(
  4. array('body' => 'Comment 1', 'user_id' => 1),
  5. array(
  6. 'body' => 'Save a new user as well',
  7. 'User' => array('first' => 'mad', 'last' => 'coder')
  8. ),
  9. ),
  10. );

并用如下语句保存该数据:

  1. $Article->saveAssociated($data, array('deep' => true));

警告

当 atomic 选项设置为 false 时,在检查 saveAssociated 的调用(返回值)时要当心,它返回数组,而不是布尔值。

传入多个模型的 fieldList 的例子:

  1. $this->SomeModel->saveAll($data, array(
  2. 'fieldList' => array(
  3. 'SomeModel' => array('field_1'),
  4. 'AssociatedModel' => array('field_2', 'field_3')
  5. )
  6. ));

(这里的) fieldList 是一个以模型别名为键,以字段数组为值的数组。模型名不同于在要保存的数据中那样,不能嵌套。

在 2.1 版更改: Model::saveAll() 和类似方法现在支持传入多个模型的 fieldList 选项。

现在你也可以设置 $options['deep'] = true; 来保存深层关联的数据。

Model::saveAll(array $data = null, array $options = array())

saveAll 函数只是 saveMany 方法和 saveAssociated 方法的包装。它会检查数据并且决定应当执行哪种类型的保存。如果数据是数字索引数组的格式,就会调用saveMany 方法,否则调用 saveAssociated 方法。

此方法接受与前面的两个方法相同的选项,基本上只是个向后兼容方法。建议(不要使用该方法,而是)根据情况使用 saveMany 方法或 saveAssociated 方法。

保存相关模型的数据(hasOne, hasMany, belongsTo)

在与关联模型一起使用时,重要的是要意识到,保存模型数据应当总是由相应的 CakePHP模型来完成。如果保存一条新的 Post 和它关联的 Comment,就要在保存操作的过程中使用Post 和 Comment 模型。

如果在系统中关联模型双方的记录都还不存在(例如,想要同时保存新的 User 及相关的Profile 记录),就需要先保存主模型或者父模型。

为了了解这是如何进行的,想像一下在 UsersController 控制器中有一个动作,处理新User 和相关 Profile 的保存。下面的示例动作假设已经(使用 FormHelper)提交(POSTed)了足够的数据,来创建单个 User 和单个 Profile:

  1. public function add() {
  2. if (!empty($this->request->data)) {
  3. // 我们可以保存 User 数据:
  4. // 它应当在 $this->request->data['User'] 中
  5.  
  6. $user = $this->User->save($this->request->data);
  7.  
  8. // 如果用户保存了,现在添加这条数据到 data 中并保存 Profile。
  9.  
  10. if (!empty($user)) {
  11. // 新创建的 User ID 已经被赋值给 $this->User->id.
  12. $this->request->data['Profile']['user_id'] = $this->User->id;
  13.  
  14. // 由于 User hasOne Profile 的关联,因此可以通过 User 模型访问
  15. // Profile 模型:
  16. $this->User->Profile->save($this->request->data);
  17. }
  18. }
  19. }

规则是,当使用 hasOne、hasMany 和 belongsTo 关联时,重要的是如何设置键。基本思路是从一个模型中获取键,并将其放入另一个模型的外键字段中。有时这可能需要在调用save() 方法之后使用模型类的 $id 属性,不过其它情况下可能只需要从刚提交(POSTed)给控制器动作的表单中的隐藏输入项(hidden input)获得 ID。

为了补充上面使用的基本方法,CakePHP 还提供了一个非常方便的方法saveAssociated(),这让你可以一次验证和保存多个模型的数据。而且,saveAssociated() 方法还提供了事务支持以确保数据库中的数据完整性(例如,如果一个模型保存失败,其它模型也不会保存)。

注解

要在 MySQL 中使事务正常工作,表必须使用 InnoDB 引擎。记住,MyISAM 表不支持事务。

来看看如何使用 saveAssociated() 方法同时保存 Company 和 Account 模型吧。

首先,需要为 Company 和 Account 模型一起创建表单(假设 Company hasMany Account):

  1. echo $this->Form->create('Company', array('action' => 'add'));
  2. echo $this->Form->input('Company.name', array('label' => 'Company name'));
  3. echo $this->Form->input('Company.description');
  4. echo $this->Form->input('Company.location');
  5.  
  6. echo $this->Form->input('Account.0.name', array('label' => 'Account name'));
  7. echo $this->Form->input('Account.0.username');
  8. echo $this->Form->input('Account.0.email');
  9.  
  10. echo $this->Form->end('Add');

注意看一下命名 Acount 模型的表单字段的方式。如果 Company 是主模型,saveAssociated() 方法期望相关模型(Account)的数据以特定的格式提供,而Account.0.fieldName 恰恰是我们需要的。

注解

上面的字段命名对于 hasMany 关联是必须的。如果模型之间的关联是 hasOne,对关联模型就要使用 ModelName.fieldName 标记方法了。

现在,可以在 CompaniesController 中创建 add() 动作了:

  1. public function add() {
  2. if (!empty($this->request->data)) {
  3. // 用下面的方法来避免验证错误:
  4. unset($this->Company->Account->validate['company_id']);
  5. $this->Company->saveAssociated($this->request->data);
  6. }
  7. }

就是这些。现在 Company 和 Account 模型将同时被验证和保存。默认情况下,saveAssociated 方法会验证所有传入的值,然后尝试对每一个进行保存。

保存通过(连接模型)的 hasMany 数据

让我们来看看如何保存两个模型的连接(join)表中的数据。就像通过(连接模型)的 hasMany 一节展示的那样,连接表用 hasMany 类型的关系关联到各个模型。在我们的例子中,Cake 学校的负责人要求我们写一个应用程序,让他可以记录一个学生在某门课上的出勤天数和分数。查看下面的代码。

  1. // Controller/CourseMembershipController.php
  2. class CourseMembershipsController extends AppController {
  3. public $uses = array('CourseMembership');
  4.  
  5. public function index() {
  6. $this->set(
  7. 'courseMembershipsList',
  8. $this->CourseMembership->find('all')
  9. );
  10. }
  11.  
  12. public function add() {
  13. if ($this->request->is('post')) {
  14. if ($this->CourseMembership->saveAssociated($this->request->data)) {
  15. return $this->redirect(array('action' => 'index'));
  16. }
  17. }
  18. }
  19. }
  20.  
  21. // View/CourseMemberships/add.ctp
  22.  
  23. <?php echo $this->Form->create('CourseMembership'); ?>
  24. <?php echo $this->Form->input('Student.first_name'); ?>
  25. <?php echo $this->Form->input('Student.last_name'); ?>
  26. <?php echo $this->Form->input('Course.name'); ?>
  27. <?php echo $this->Form->input('CourseMembership.days_attended'); ?>
  28. <?php echo $this->Form->input('CourseMembership.grade'); ?>
  29. <button type="submit">Save</button>
  30. <?php echo $this->Form->end(); ?>

提交的数据数组如下。

  1. Array
  2. (
  3. [Student] => Array
  4. (
  5. [first_name] => Joe
  6. [last_name] => Bloggs
  7. )
  8.  
  9. [Course] => Array
  10. (
  11. [name] => Cake
  12. )
  13.  
  14. [CourseMembership] => Array
  15. (
  16. [days_attended] => 5
  17. [grade] => A
  18. )
  19.  
  20. )

在 CakePHP 中,使用这种数据结构调用 saveAssociated 方法,就能够很容易地同时保存这么多数据,并将 Student 和 Course 的外键赋值到 CouseMembership 内。如果我们运行 CourseMembershipsController 的 index 动作,从 find(‘all’) 中获取的数据结构就会是:

  1. Array
  2. (
  3. [0] => Array
  4. (
  5. [CourseMembership] => Array
  6. (
  7. [id] => 1
  8. [student_id] => 1
  9. [course_id] => 1
  10. [days_attended] => 5
  11. [grade] => A
  12. )
  13.  
  14. [Student] => Array
  15. (
  16. [id] => 1
  17. [first_name] => Joe
  18. [last_name] => Bloggs
  19. )
  20.  
  21. [Course] => Array
  22. (
  23. [id] => 1
  24. [name] => Cake
  25. )
  26. )
  27. )

当然,还有很多使用连接模型的方式。上面的方式假定你想要一次保存所有数据。存在这样的情况,你想单独地创建 Student 和 Course,稍后再把两者与 CourseMembership 关联起来。这样你可能有一个表单,允许通过现有学生和课程的列表或者 ID 输入项进行选择,以及 CourseMembership 的两个字段,例如:

  1. // View/CourseMemberships/add.ctp
  2.  
  3. <?php echo $this->Form->create('CourseMembership'); ?>
  4. <?php
  5. echo $this->Form->input(
  6. 'Student.id',
  7. array(
  8. 'type' => 'text',
  9. 'label' => 'Student ID',
  10. 'default' => 1
  11. )
  12. );
  13. ?>
  14. <?php
  15. echo $this->Form->input(
  16. 'Course.id',
  17. array(
  18. 'type' => 'text',
  19. 'label' => 'Course ID',
  20. 'default' => 1
  21. )
  22. );
  23. ?>
  24. <?php echo $this->Form->input('CourseMembership.days_attended'); ?>
  25. <?php echo $this->Form->input('CourseMembership.grade'); ?>
  26. <button type="submit">Save</button>
  27. <?php echo $this->Form->end(); ?>

所得到的 POST 数据为:

  1. Array
  2. (
  3. [Student] => Array
  4. (
  5. [id] => 1
  6. )
  7.  
  8. [Course] => Array
  9. (
  10. [id] => 1
  11. )
  12.  
  13. [CourseMembership] => Array
  14. (
  15. [days_attended] => 10
  16. [grade] => 5
  17. )
  18. )

利用 saveAssociated 方法,CakePHP 仍然可以很容易地把 Student id 和 Course id放入 CourseMembership 中。

保存相关模型数据 (HABTM)

保存通过 hasOne、belongsTo 和 hasMany 关联的模型非常简单:只需要将关联模型的 ID填入外键字段。 一旦完成,只要调用模型的 save() 方法,所有数据就被正确地连接起来了。下面的示例是传递给 Tag 模型的 save() 方法的数据数组的格式:

  1. Array
  2. (
  3. [Recipe] => Array
  4. (
  5. [id] => 42
  6. )
  7. [Tag] => Array
  8. (
  9. [name] => Italian
  10. )
  11. )

也可以使用这种格式调用 saveAll() 来保存多条记录和它们的 HABTM 关联(模型),使用下面这样的数组:

  1. Array
  2. (
  3. [0] => Array
  4. (
  5. [Recipe] => Array
  6. (
  7. [id] => 42
  8. )
  9. [Tag] => Array
  10. (
  11. [name] => Italian
  12. )
  13. )
  14. [1] => Array
  15. (
  16. [Recipe] => Array
  17. (
  18. [id] => 43
  19. )
  20. [Tag] => Array
  21. (
  22. [name] => Pasta
  23. )
  24. )
  25. [2] => Array
  26. (
  27. [Recipe] => Array
  28. (
  29. [id] => 51
  30. )
  31. [Tag] => Array
  32. (
  33. [name] => Mexican
  34. )
  35. )
  36. [3] => Array
  37. (
  38. [Recipe] => Array
  39. (
  40. [id] => 17
  41. )
  42. [Tag] => Array
  43. (
  44. [name] => American (new)
  45. )
  46. )
  47. )

将上面的数组传递给 saveAll() 方法将创建所包含的标签(tag),各自与它们相应的菜单(recipe)关联。

另一个有用的例子是,当需要保存多个标签(Tag)到文章(Post)中。这需要用以下的HABTM 数组格式传入关联的 HABTM 数据。注意,只需要传入关联的 HABTM 模型的 id,不论需要再怎样嵌套:

  1. Array
  2. (
  3. [0] => Array
  4. (
  5. [Post] => Array
  6. (
  7. [title] => 'Saving HABTM arrays'
  8. )
  9. [Tag] => Array
  10. (
  11. [Tag] => Array(1, 2, 5, 9)
  12. )
  13. )
  14. [1] => Array
  15. (
  16. [Post] => Array
  17. (
  18. [title] => 'Dr Who\'s Name is Revealed'
  19. )
  20. [Tag] => Array
  21. (
  22. [Tag] => Array(7, 9, 15, 19)
  23. )
  24. )
  25. [2] => Array
  26. (
  27. [Post] => Array
  28. (
  29. [title] => 'I Came, I Saw and I Conquered'
  30. )
  31. [Tag] => Array
  32. (
  33. [Tag] => Array(11, 12, 15, 19)
  34. )
  35. )
  36. [3] => Array
  37. (
  38. [Post] => Array
  39. (
  40. [title] => 'Simplicity is the Ultimate Sophistication'
  41. )
  42. [Tag] => Array
  43. (
  44. [Tag] => Array(12, 22, 25, 29)
  45. )
  46. )
  47. )

把上面的数组传入 saveAll($data, array('deep' => true)),会在 posts_tags 连接表中填入 Tag 和 Post 之间的关联。

作为示例,我们来创建一个表单,用来创建新的标签(tag),动态生成正确的数据数组与某个菜单(recipe)关联。

最简单的表单可以象这样(我们假定 $recipe_id 已经设置为某值了):

  1. <?php echo $this->Form->create('Tag'); ?>
  2. <?php echo $this->Form->input(
  3. 'Recipe.id',
  4. array('type' => 'hidden', 'value' => $recipe_id)
  5. ); ?>
  6. <?php echo $this->Form->input('Tag.name'); ?>
  7. <?php echo $this->Form->end('Add Tag'); ?>

在这个例子中,你可以看到 Recipe.id 隐藏字段的值被设置为 tag 要连接的 recipe的 ID。

当在控制器中调用 save() 方法时,它将自动将 HABTM 数据保存到数据库:

  1. public function add() {
  2. // Save the association
  3. if ($this->Tag->save($this->request->data)) {
  4. // do something on success
  5. }
  6. }

调用上面这段代码,将创建新的 Tag 并与 Recipe 相关联,其 ID 为$this->request->data['Recipe']['id']

其它我们可能希望呈现关联数据的方式,可以包括下拉列表。数据可以使用find('list') 方法从模型中取出,并且赋给用模型名命名的视图变量。同名的输入项(input)会自动把该数据放入 <select> 元素中:

  1. // 在控制器中:
  2. $this->set('tags', $this->Recipe->Tag->find('list'));
  3.  
  4. // 在视图中:
  5. $this->Form->input('tags');

HABTM 关系更可能的情形会包含一个允许多选的 <select> 元素。例如,一个菜单(Recipe)可以被贴上多个标签(Tag)。在这种情况下,数据以相同的方式从模型中取出,但是表单的输入项(input)定义稍有不同。tag 名称使用 ModelName 约定来定义:

  1. // 在控制器中:
  2. $this->set('tags', $this->Recipe->Tag->find('list'));
  3.  
  4. // 在视图中:
  5. $this->Form->input('Tag');

使用上面这段代码,会创建多选的下拉列表(drop down),允许多个选项自动被保存到数据库中已添加或已保存的现有 Recipe 上。

自我 HABTM

通常 HABTM 关联用于绑定2个模型,但是它也可以用于1个模型,不过这需要更加小心。

关键在于模型的设置 className。简单地添加 Project HABTM Project 关联会引起保存数据时的错误。设置 className 为模型名称,并用别名作为键,就避免了这些问题。

  1. class Project extends AppModel {
  2. public $hasAndBelongsToMany = array(
  3. 'RelatedProject' => array(
  4. 'className' => 'Project',
  5. 'foreignKey' => 'projects_a_id',
  6. 'associationForeignKey' => 'projects_b_id',
  7. ),
  8. );
  9. }

创建表单元素,保存数据,都象以前一样,但是要使用别名。这样的代码:

  1. $this->set('projects', $this->Project->find('list'));
  2. $this->Form->input('Project');

就变成这样:

  1. $this->set('relatedProjects', $this->Project->find('list'));
  2. $this->Form->input('RelatedProject');

当 HABTM 变得复杂时怎么办?

默认情况下,当保存 HasAndBelongsToMany 关系时,在保存新行之前 CakePHP 会先删除连接表中的所有(相关)行。 例如,一个 Club 有10个相关的 Children,然后更新 Club 为只有2个 children。这样,Club 将只有2个 Children,而不是12个。

也要注意,如果想要在连接中加入更多字段(何时创建或者其它数据),这在使用 HABTM 连接表时是可能的,不过重要的是要明白你有简单的解决办法。

两个模型间的 HasAndBelongsToMany 关联实际上是同时通过 hasMany 和 belongsTo 关联的三个模型关系的简写。

考虑这个例子:

  1. Child hasAndBelongsToMany Club

另一种看待它的方法是添加一个 Membership 模型:

  1. Child hasMany Membership
  2. Membership belongsTo Child, Club
  3. Club hasMany Membership.

这两个例子几乎是完全相同的。它们在数据库中使用了相同数量的命名字段,相同数量的模型。重要的区别是,"连接(join)" 模型命名不同,并且其行为更容易预知。

小技巧

当连接表包含两个外键以外的额外字段时,通过将数组的键 'unique' 设置为'keepExisting',能够防止丢失额外字段的值。你可以认为这与设置'unique' => true 类似,但在保存操作过程中不会丢失额外字段的数据。另外,如果你使用 bake 来创建模型,自动会设置成这样。参见HABTM 关联数组

不过,在大多数情况下,象上面的例子那样为连接表建立模型,设置 hasMany、belongsTo关联,比使用 HABTM 关联更简单。

数据库表

虽然 CakePHP 可以有非数据库驱动的数据源,但大多数时候是数据库驱动的。CakePHP 被设计成与(数据库)无关,可以使用 MySQL、Microsoft SQL Server、PostgreSQL 和其它数据库。你可以象平时那样创建数据库表。在创建模型类时,模型将自动映射到你创建的表上。按照约定,表名为小写、复数,多个单词的表名用下划线分隔。例如,名为 Ingredient 的模型对应的表名为 ingredients。名为 EventRegistration 的模型对应的表名为 event_registrations。CakePHP 会检视表来决定每个字段的数据类型,并使用这些信息自动化各种特性,比如输出视图中的表单字段。按照约定,字段名为小写并用下划线分隔。

使用 created 和 modified 列

如果在数据库表中定义 created 和/或 modified 字段为 datetime 字段(缺省值null),CakePHP 能够识别这些字段,每当创建或保存一条记录到数据库时,自动填入这两个字段(除非要保存的数据中已经包含了这两个字段的值)。

在最初添加记录时,createdmodified 字段会被设置为当前日期和时间。每当保存现有记录时,modified 字段会被更新为当前日期和时间。

如果在调用 Model::save() 之前 ,$this->data 中包含了 createdmodified字段的数据(例如来自 Model::read 或者 Model::set 方法),那么这些值将从$this->data 中获取,而不会自动魔法更新。如果不希望那样,可以用unset($this->data['Model']['modified']) 等。另一种方法,可以重载Model::save() 方法来帮你总是这么做:

  1. class AppModel extends Model {
  2.  
  3. public function save($data = null, $validate = true, $fieldList = array()) {
  4. // 在每次调用 save 方法前清除 modified 字段值:
  5. $this->set($data);
  6. if (isset($this->data[$this->alias]['modified'])) {
  7. unset($this->data[$this->alias]['modified']);
  8. }
  9. return parent::save($this->data, $validate, $fieldList);
  10. }
  11.  
  12. }

如果你保存数据时设置了 fieldList,并且 createdmodified 不在允许保存的字段列表中,这两个字段还是会自动设置。当 createdmodified 包括在fieldList 中时,它们就和任何别的字段一样处理(即,不会自动设置)。