Models
Section 1 What is a model?
什么是model呢?model就是MVC模式中的M。
Model用来做什么呢?它将领域逻辑从表现层和独立的业务逻辑中剥离出来。
Model 通常是数据库的访问介质,而且更明确的指定于某一张表(译注:在rails框架中model扮演着active record的角色,因此同时承担着DAO对象的职责)。默认情况下,model使用的表名为model名字的复数形式,比如'User'这个model 对应的表名为'users'。Model可以包含数据校验规则,关联关系以及针对这张表的业务逻辑。下面展示了User model在cake中的具体定义:
- Example User Model, saved in /app/models/user.php
- <?php
- //AppModel gives you all of Cake's Model functionality
- class User extends AppModel
- {
- // Its always good practice to include this variable.
- var $name = 'User';
- // This is used for validation, see Chapter 11.
- var $validate = array();
- // You can also define associations.
- // See section 6.3 for more information.
- var $hasMany = array('Image' =>
- array('className' => 'Image')
- );
- // You can also include your own functions:
- function makeInactive($uid)
- {
- //Put your own logic here...
- }
- }
- ?>
(译注:关于Model应该是贫血型,失学型还是充血型的论战持续至今尚无明显得胜的一方,本文中的这句话:Model将领域逻辑从表现层和独立的业务逻辑中剥离出来 说得还比较清晰的,起码在cake里面他就是这么遵循的)
Section 2 Model Functions
从PHP 的角度来看Model,只是一个继承了AppModel的类。AppModel类在/cake目录中已经定义了,如果你希望创建一个自己的 AppModel基类,你可以放在app/app_model.php位置。AppModel中定义的方法将被所有model所继承并共享。而 AppModel则继承于Cake Lib中的Model类,文件为cake/libs/model.php。
本章将会讨论到大量已经在Cake Model类中定义的方法,所以这里提醒,http://api.cakephp.org 将是您非常重要的参考。
用户定义的Model函数
下面是一个用户定义的Model方法,是帖子的隐藏/显示方法:
- Example Model Functions
- <?php
- class Post extends AppModel
- {
- var $name = 'Post';
- function hide ($id=null)
- {
- if ($id)
- {
- $this->id = $id;
- $this->saveField('hidden', '1');
- }
- }
- function unhide ($id=null)
- {
- if ($id)
- {
- $this->id = $id;
- $this->saveField('hidden', '0');
- }
- }
- }
- ?>
获取你需要的数据
下面是Model中一些获取你需要的数据的标准方法:
findAll
string $conditions
array $fields
string $order
int $limit
int $page
int $recursive
[$conditions Type: string] 检索条件,就是sql中where子句,就像这样 $conditions = "race = 'wookie' AND thermal_detonators > 3"。
[$fields Type: array] 检索属性,就是投影,指定所有你希望返回的属性。
(译注:这里我没有使用字段这个亲SQL的名字而是用了属性这个亲对象的名字,我相信很多PHPer更熟悉基于Sql和DTO的开发模式,但是引入Model的一个初衷就是将关系数据库转换为对象操作,所以投影查询应该理解为检索对象的某些属性)
[$order Type: string] 排序属性 指定了oder by的属性名 [TODO:check whether the multiple order field be supported]
[$limit Type: int] 结果集数据条目限制
[$page Type: int] 结果集分页index,默认为返回第一页
[$recursive Type: int] 递归
当设为大于1的整数时,将返回该model关联的其他对象。(译注:如果你使用过类似于Hibernate这样的ORM工具的话,不难理解这个属性就是
LazyLoad属性,但也不完全等同,数字的值代表级联查询的层次数,例如这样,user.country.city.address.id)
find
string $conditions
array $fields
string $order
int $recursive
find方法和findAll方法的区别在于,findAll方法返回所有符合的结果集,find方法只返回list中的第一个结果。
findAllBy<fieldName>
string $value
这是一个非常有魔力的方法,你可以看成是一个快速按照某个属性检索数据的方法,下面是在controller中的一段示例代码:
- $this->Post->findByTitle('My First Blog Post');
- $this->Author->findByLastName('Rogers');
- $this->Property->findAllByState('AZ');
- $this->Specimen->findAllByKingdom('Animalia');
返回类型和find() findAll()一样,是一个array。
findNeighbours
string $conditions
array $field
string $value
这 个方法首先是通过conditions过滤获取结果集,然后根据field=value查找符合的对象(仅包含$field中指定的属性),最终返回该对 象以及该对象的上一个对象和下一个对象。这在诸如相册这样的应用场景中将会非常有作用,你可以方便的获取当前相片,上一张和下一张这样的结果集合。注意: 该方法只能作用于数值型和日期时间型属性。下面是示例代码:
- class ImagesController extends AppController
- {
- function view($id)
- {
- // Say we want to show the image...
- $this->set('image', $this->Image->find("id = $id");
- // But we also want the previous and next images...
- $this->set('neighbours', $this->Image->findNeighbours(null, 'id', $id);
- }
- }
我们拿到了包含一个完整$image['Image']对象的array,以及$neighbours['prev']['Image']['id']和$neighbours['next']['Image']['id']。
field
string $name
string $conditions
string $order
返回conditions过滤后按order排序的第一个结果中的name属性值,返回类型为string。
findCount
string $conditions
返回conditions过滤后结果集的数量。即select count .....
generateList
string $conditions
string $order
int $limit
string $keyPath
string $valuePath
generateList 方法可以非常快捷的获取一个key-value这样的list,其实就是其他语言中的map类型,在web领域中,下拉框可能将是该方法最大的受益者。$ conditions, $order 和 $limit这3个参数的用法和findAll()没有区别,$keyPath 和 $valuePath是model中用来填充结果集中key和value的属性。举个例子,在权限操作中,角色的定义往往是id - name这样的组合,参见下面的示例代码:
- $this->set(
- 'Roles',
- $this->Role->generateList(null, 'role_name ASC', null, '{n}.Role.id', '{n}.Role.role_name')
- );
- //This would return something like:
- array(
- '1' => 'Account Manager',
- '2' => 'Account Viewer',
- '3' => 'System Manager',
- '4' => 'Site Visitor'
- );
read
string $fields
string $id
根 据$fields中的属性名从当前对象中读出对应的值,或者是$id指定的对象中读取。注意:read()方法仅级联读取该对象的一级关联对象,不管 model中的$recursive的值为多少。如果你是希望获取所有级联属性的话,请使用find()或者findAll()方法。
query
string $query
execute
string $query
有 的时候希望执行自定义的sql语句或者是出于性能上的考虑要优化sql语句,则可以使用query(string $query)和execute(string $query)方法,这两个方法不同的地方在于execute方法无返回值,适用于执行脚本而不是查询结果集。
- Custom Sql Calls with query()
- <?php
- class Post extends AppModel
- {
- var $name = 'Post';
- function posterFirstName()
- {
- $ret = $this->query("SELECT first_name FROM posters_table
- WHERE poster_id = 1");
- $firstName = $ret[0]['first_name'];
- return $firstName;
- }
- }
- ?>
复合查询条件(使用array)
绝 大多数的Model查询方法中都会通过各种方式来传递一组条件。最简单的方式莫过于使用一个SQL中的where子句,不过如果你希望获得更多的控制,你 可以使用array。使用rray的好处是代码会非常的清晰和易读,并且可以比较方便的构造查询。而且这样的语法可以保证你的查询组成元素(属性,值,运 算符等)被拆分为精巧可控的几部分。这可以保证Cake能够生成绝大部分非常有效率的查询语句,并且避免了SQL语法上易犯的错误。
先看看非常基础的基于array的查询:
简单查询条件array示例:
$conditions = array("Post.title" => "This is a post");
这 个结构恐怕是不言自明了:该查询返回所有title为"This is a post"的Post对象。注意,只使用"title"作为查询的属性名也是可以的,但是在构造查询的时候,使用 [ModelName.FieldName]这样的写法,一者可以让你的代码更加清晰,二者可以防止命名冲突,所以请遵循这个Good Practice。那其他的检索形式如何呢?同样很简单,比如我们想检索所有title不是"This is a post"的Post对象:
array("Post.title" => "<> This is a post")
唯一增加的就是'<>'这个表达式。Cake能够解析所有的合法SQL比较运算符,包括LIKE, BETWEEN或者是正则表达式,注意,运算符和值或者表达式之间需要有一个空格。唯一不同的操作是IN操作。我们看下具体的例子:
array("Post.title" => array("First post", "Second post", "Third post"))
为conditions增加更多filter就像往array中添加一对key-value那样简单:
- array
- (
- "Post.title" => array("First post", "Second post", "Third post"),
- "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks"))
- )
默认情况下,Cake使用'AND'来连接多个条件;这意味着,上面的示例代码检索结果为过去2周内title为"First post", "Second post"或者"Third post"的记录。当然我们也同样可以使用其他的逻辑运算符来连接查询条件:
- array
- ("or" =>
- array
- (
- "Post.title" => array("First post", "Second post", "Third post"),
- "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks"))
- )
- )
Cake 可以使用任何SQL语句中允许的逻辑运算符,如AND, OR, NOT, XOR等等,并且大小写不敏感(Conditions可以无限嵌套,但我并不推荐使用这样的magic code)。Posts和Authors两个对象间分别是hasMany/belongsTo的关系(熟悉Hibernate的同学或许更喜欢一对多 多对一的叫法)。假设你希望检索所有过去两周内发布的posts或者是包含有指定关键字的posts,并且你希望限定作者为Bob,让我们看如下代码:
- array
- ("Author.name" => "Bob", "or" => array
- (
- "Post.title" => "LIKE %magic%",
- "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks")
- )
- )
保存数据
当需要保存model对象时(译注:或者使用持久化这个词更贴切),你需要提供如下形式的数据给sava()方法:
- Array
- (
- [ModelName] => Array
- (
- [fieldname1] => 'value'
- [fieldname2] => 'value'
- )
- )
为
了能够让数据通过这样的形式提交给controller,最方便的办法就是使用HTML Helper来完成这个任务,因为HTML
Helper会为提交的Form封装成Cake希望的这种形式。当然你可以不使用,只要页面上的元素的name被设置成date[Modelname]
[fieldname]形式就ok了。不过我还是要说,$html->input('Model/fieldname')是最方便的办法。
(译注:OGNL的php版,太棒了,我的意见是如果不嫌麻烦的话尽量使用类似OGNL的做法,因为tag产生的页面在设计期是无法预览的,我相信web应用前台页面的设计的复杂性并不亚于后台业务逻辑)
从页面Form中提交的数据自动被格式化并且注入到controller中的$this->data变量,所以保存从web页面提交的数据只是举手之劳。下面我们来看一段示例代码:
- function edit($id)
- {
- //Note: The property model is automatically loaded for us at $this->Property.
- // Check to see if we have form data...
- if (emptyempty($this->data))
- {
- $this->Property->id = $id;
- $this->data = $this->Property->read();//populate the form fields with the current row
- }
- else
- {
- // Here's where we try to save our data. Automagic validation checking
- if ($this->Property->save($this->data['Property']))
- {
- //Flash a message and redirect.
- $this->flash('Your information has been saved.',
- '/properties/view/'.$this->data['Property']['id'], 2);
- }
- //if some fields are invalid or save fails the form will render
- }
- }
注意保存数据前会触发model的校验机制,更多关于数据校验的内容请参见Chapter 12。如果你不希望进行数据校验,可以使用save($date, false)。
其他一些有用的数据保存函数
del
string $id
boolean $cascade
删除指定id的model对象,或者当前model对象。
如果$cascade设为true,则会级联删除当前model所关联的所有model中,设为'dependent'的model对象。删除成功返回true
saveField
string $name
string $value
保存单个属性值。
getLastInsertId
返回最后当前ID属性的nextValue。
[ToDo 扩展支持多种主键生成策略 例如sequence UUID]
Model中的回调函数
我 们在model中增加了一些回调函数以帮助你在model操作前后能够织入一些业务逻辑(原文为sneak in,借用了AOP中的织入一词,因为从操作来看这些回调函数等同于AOP中的advice)。为了获得这样的能力,需要使用model中的一些参数并且 在你的model中覆写这些方法。
beforeFind
string $conditions
beforeFind ()回调函数是在model的find方法执行前的前置操作。你可以加入任何检索前的业务逻辑。你覆写该方法只要保证在前置操作成功后返回true来执行 真正的find方法,返回false中断find方法就可以了。(译注:在一些复杂场景中,需多次持久化的情况下请慎用)
afterFind
array $results
使用afterFind回调函数能够更改find方法的返回结果集,或者在检索动作完成后加上一些业务逻辑。该函数的参数$results为经过回调函数处理以后的find检索结果集。
beforeValidate
beforeValidate 回调函数能够在model校验数据之前更改model中的一些数据。同样也可以用来在model校验之前加入更为复杂的额外校验规则。和 beforeFind一样,必须保证返回true来调用真正的操作,返回false来中断校验乃至save操作。
beforeSave
和先前介绍的回调函数类似,在校验完成之后,保存动作之前加入额外的处理(如果校验失败是不会触发该回调函数的)。返回true或者false,不再赘述。
一个比较常见的beforeSave的应用场景就是在保存动作之前格式化日期属性以适应不同的数据库:
- // Date/time fields created by HTML Helper:
- // This code would be seen in a view
- $html->dayOptionTag('Event/start');
- $html->monthOptionTag('Event/start');
- $html->yearOptionTag('Event/start');
- $html->hourOptionTag('Event/start');
- $html->minuteOptionTag('Event/start');
- /*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-*/
- // Model callback functions used to stitch date
- // data together for storage
- // This code would be seen in the Event model:
- function beforeSave()
- {
- $this->data['Event']['start'] = $this->_getDate('Event', 'start');
- return true;
- }
- function _getDate($model, $field)
- {
- return date('Y-m-d H:i:s', mktime(
- intval($this->data[$model][$field . '_hour']),
- intval($this->data[$model][$field . '_min']),
- null,
- intval($this->data[$model][$field . '_month']),
- intval($this->data[$model][$field . '_day']),
- intval($this->data[$model][$field . '_year'])));
- }
afterSave
保存完成后执行的动作。[ToDo 如果保存出错是否会触发?]
beforeDelete
afterDelete
不需要多说了吧,删除操作前后的回调函数。
Section 3 Model中的变量
当你创建一个新的model的时候,有很多特殊的变量可以设置,从而是model具备Cake赋予的相应操作。
$primaryKey
如果主键字段不为'id',COC无法发挥的时候,你可以通过该变量来指定主键字段名字。
$recursive
这个我们上面已经介绍过了,指定了model级联关联对象的深度。
想象这样一个场景,Group下的User下面还有各自的Articles。
$recursive = 0 Cake只会检索Group对象
$recursive = 1 Cake会检索包含User对象的Group对象
$recursive = 2 Cake会检索完成的包含Article的Group对象
$transactional
决定Model是否允许事务处理,true 或者 false。注意,仅在支持事务的数据库上有效。
$useTable
和$primaryKey一样,如果你的表名不能和model匹配的话,而且你也不想或者不能修改表名,则可以通过该变量来指定数据表名。
$validate
该变量为一个array,表示一组校验规则,详细请参见Chapter 12。
$useDbConfig
还记得我们在配制一章介绍的如何配置数据库连接变量吗?可以通过该变量非常方便的切换数据库连接,默认的是什么?猜一下,当然就是'default',rails就是让你忘却那些难以记忆的配置。
Section 4 [重头戏]关联对象
简介
CakePHP中提供的一个非常重要的功能就是关联数据表间的映射。在CakePHP中,关联表通过association(关联)来处理。关联是这些逻辑上相关的单元间的胶水一般。
CakePHP中一共有4种类型的关联:
hasOne
hasMany
belongsTo
hasAndBelongsToMany
一 旦model间的关联关系被定义,Cake能够自动检索你当前操作的model中包含的关联对象。也就是将基于关系数据库的数据映射为基于对象的领域模 型。举个例子,Post和Author之间是hasMany(一对多)关系,当我们在controller中通过$this->Post-> findAll()来检索所有的Post对象时,也会同时把所有关联的Author对象检索出来。
遵循CakePHP的命名约 定是正确定义关联关系的有利保证(参见附录 B)。如果你使用了CakePHP的命名约定,可以通过脚手架来可视化你的数据,因为脚手架会自动侦测并使用你定义的关联关系,这样也能某种程度上供你检 查是否定义正确。当然,你也可以完全不使用命名约定来定义关联,不过我们稍候再介绍关于这种定义。现在我们仅关注使用命名约定的情况下的关联定义。命名约 定中我们需要考虑的有这3个内容,外键,model名字,表名。
这里先简单的介绍一下关于这3者的要求,更为详细的请查看附录:
外键:[单数形式的model名字]_id 假设在"authors"表中有Post的外键关联,则外键字段名字应该为 "post_id"。
表名:[复数形式的model名字] 表名必须为model名字的复数形式,例如:"posts" "authors"。
Model的名字:[驼峰法命名 单数形式]。
CakePHP 的脚手架希望你的关联定义的顺序和表中的外键字段的顺序是一致的。所以如果我有一个Article对象[belongsTo(属于)]另外3个对象 (Author Editor Publisher)的话,我需要3个外键author_id, editor_id, publisher_id。脚手架要求你model中对应的关联和数据库中的列顺序保持一致。
为了更好的描述关联对象是如何运 作的,让我们继续以Blog为应用场景来介绍。假设我们现在需要为Blog系统创建一个简单的用户管理模块。我们假设我们不需要跟踪用户情况,但是我们希 望每个用户都一个个人记录(用户 [hasOne] 个人记录)。用户可以创建多条评论记录(用户 [hasMany] 评论记录)。同样的,我们的文章会被分配到多个tag,同时每个tag都包含多篇文章,也就是多对多关系 (文章 [hasAndBelongsToMany] Tag)。
hasOne 关联的定义与查询
假设你已经准备好了User和Profile两个model,让我们来定义他们之间的关联。hasOne关联的定义是通过在model中增加一个array来实现的。下面是示例代码:
- /app/models/user.php hasOne
- <?php
- class User extends AppModel
- {
- var $name = 'User';
- var $hasOne = array('Profile' =>
- array('className' => 'Profile',
- 'conditions' => '',
- 'order' => '',
- 'dependent' => true,
- 'foreignKey' => 'user_id'
- )
- );
- }
- ?>
$hasOne变量是一个array,Cake通过该变量来构建User与Profile之间的关联。我们来看每一个元素代表的意义:
- className (required):关联对象的类名,上面代码中我们设为'Profile'表示关联的是Profile对象。
- conditions: 关联对象的选择条件,(译注:类似hibernate中的formula)。具体到我们的例子来看,假设我们仅关联Profile的header color为绿色的文件记录,我们可以这样定义conditions,"Profile.header_color = 'green'"。
- order: 关联对象的排序方式。假设你希望关联的对象是经过排序的,你可以为order赋值,就如同SQL中的order by子句:"Profile.name ASC"。
- dependent:这是个布尔值,如果为true,父对象删除时会级联删除关联子对象。在我们的Blog中,如果"Bob"这个用户被删除了,则关联的Profile都会被删除。类似一个外键约束。
- foreignKey:指向关联Model的外键字段名。仅在你不遵循Cake的命名约定时需要设置。
现在,现在当我们使用find() findAll()检索User对象时,你会发现关联的Profile对象也被检索回来,非常的方便:
- $user = $this->User->read(null, '25');
- print_r($user);
- //output:
- Array
- (
- [User] => Array
- (
- [id] => 25
- [first_name] => John
- [last_name] => Anderson
- [username] => psychic
- [password] => c4k3roxx
- )
- [Profile] => Array
- (
- [id] => 4
- [name] => Cool Blue
- [header_color] => aquamarine
- [user_id] = 25
- )
- )
belongsTo关联的定义与使用
现在User对象能够得到对应的Profile对象,当然我们也应该为Profile对象定义一个关联使之能够获取它的所有者,也就是对应的User对象。在Cake中,我们使用belongsTo关联来实现:
- /app/models/profile.php belongsTo
- <?php
- class Profile extends AppModel
- {
- var $name = 'Profile';
- var $belongsTo = array('User' =>
- array('className' => 'User',
- 'conditions' => '',
- 'order' => '',
- 'foreignKey' => 'user_id'
- )
- );
- }
- ?>
和hasOne关联一样,belongsTo也是一个array变量,你可以通过设置其中的值来具体定义belongsTo关联:
- className (required): 关联对象的类名,这里我们关联的是User对象,所以应该是'User'。
- conditions: SQL条件子句以限定关联的对象,假定我们只允许Profile关联状态为active的用户,我们可以这样写:"User.active = '1'",当然也可以是类似的其它条件。
- order:关联对象的排序子句,假如你希望关联的对象经过排序,你可以类似"User.last_name ASC"这样来定义。
- foreignKey:关联对象所对应的外键字段名,仅在你不遵循Cake的命名约定时需要设置。
现在当我们使用find() findAll()来检索Profile对象时,会发现关联的User对象也一同被检索回来。
- $profile = $this->Profile->read(null, '4');
- print_r($profile);
- //output:
- Array
- (
- [Profile] => Array
- (
- [id] => 4
- [name] => Cool Blue
- [header_color] => aquamarine
- [user_id] = 25
- )
- [User] => Array
- (
- [id] => 25
- [first_name] => John
- [last_name] => Anderson
- [username] => psychic
- [password] => c4k3roxx
- )
- )
hasMany关联的定义与查询
我们已经为User和Profile对象建立起了双向关联,那让我们开始为User和Comment对象之间建立关联吧,先看下面的示例代码:
- /app/models/user.php hasMany
- <?php
- class User extends AppModel
- {
- var $name = 'User';
- var $hasMany = array('Comment' =>
- array('className' => 'Comment',
- 'conditions' => 'Comment.moderated = 1',
- 'order' => 'Comment.created DESC',
- 'limit' => '5',
- 'foreignKey' => 'user_id',
- 'dependent' => true,
- 'exclusive' => false,
- 'finderQuery' => ''
- )
- );
- // Here's the hasOne relationship we defined earlier...
- var $hasOne = array('Profile' =>
- array('className' => 'Profile',
- 'conditions' => '',
- 'order' => '',
- 'dependent' => true,
- 'foreignKey' => 'user_id'
- )
- );
- }
- ?>
$hasMany array用来定义User包含多条Comment这样的关联关系。还是老样子,介绍一下包含的key,但是一些和之前同样含义的key我将不再赘述详细。
- className (required):关联对象类名。
- conditions: 关联对象限定条件。
- order: 关联对象排序子句。
- limit:因为是一对多关系,所以可以通过limit来限定检索的关联对象数量。比如我们可以只关联5条评论记录。
- foreignKey:外键字段名。仅当不遵循命名约定时起用。
- dependent:是否级联删除。(该动作可能会造成数据的误删除,请谨慎设定)
- exclusive: 如果设为true,所有的关联对象将在一句sql中删除,model的beforeDelete回调函数不会被执行。但是如果没有复杂的逻辑在级联删除 中,这样的设定会带来性能上的优势。(译注:Cake的确方便,但是使用时一定要记住控制sql语句发送数量)
- finderQuery: 定义一句完整的sql语句来检索关联对象,能够对关联规则进行最大程度上的控制。当关联关系特别复杂的时候,比如one table - many model one model - many table的情况下,Cake无法准确的替你完成映射动作,需要你自己来完成这个艰巨的任务。
现在看一下如何在检索user对象的时候一并读回comment对象集合
- $user = $this->User->read(null, '25');
- print_r($user);
- //output:
- Array
- (
- [User] => Array
- (
- [id] => 25
- [first_name] => John
- [last_name] => Anderson
- [username] => psychic
- [password] => c4k3roxx
- )
- [Profile] => Array
- (
- [id] => 4
- [name] => Cool Blue
- [header_color] => aquamarine
- [user_id] = 25
- )
- [Comment] => Array
- (
- [0] => Array
- (
- [id] => 247
- [user_id] => 25
- [body] => The hasMany assocation is nice to have.
- )
- [1] => Array
- (
- [id] => 256
- [user_id] => 25
- [body] => The hasMany assocation is really nice to have.
- )
- [2] => Array
- (
- [id] => 269
- [user_id] => 25
- [body] => The hasMany assocation is really, really nice to have.
- )
- [3] => Array
- (
- [id] => 285
- [user_id] => 25
- [body] => The hasMany assocation is extremely nice to have.
- )
- [4] => Array
- (
- [id] => 286
- [user_id] => 25
- [body] => The hasMany assocation is super nice to have.
- )
- )
- )
你同样可以为Comment加上关联User对象的belongsTo关联,但是在文档中就不再详细描述了。
hasAndBelongsToMany关联的定义与查询
我 相信你已经掌握了简单的关联定义,让我们来看最后一个,也是最为复杂的关联关系:hasAndBelongsToMany(HABTM)。这个关联会让你 头大的,不过也是最有用的。(译注:我倒认为应该数据库设计上尽量的避免出现大量的多对多关联,有的时候多对多关联可以比较简单拆分为两个一对多关联。) HABTM关联也就是3张表的关联关系,关系数据库中应该说只存在多对一外键关联,所以如果要做多对多关联必然需要一张关联表来保存关联关系。
hasMany 和hasAndBelongsToMany的不同处在于,hasMany关联所关联的对象只会属于本对象,不会同时属于其他对象。但是HABTM不同,所 关联的对象同时会被其他对象所关联持有。比如Post和Tag之间的关联就是这种关系,一篇日志可以属于多个不同的Tag,一个Tag也会包含多篇不同的 日志。
为了实现多对多关联,首先要建立那张关联关系表(参照表)。除了"tags" "posts"表以外,根据Cake的命名约定,关联表的名字应该是[复数形式的model1名字]_[复数形式的model2名字],至于两个model谁先谁后则根据字典排序法。
下面是一些示例:
Posts and Tags: posts_tags
Monkeys and IceCubes: ice_cubes_monkeys
Categories and Articles: articles_categories
关联表至少需要两个关联对象的外键字段,例如"post_id" 和 "tag_id"。当然你也可以加入一些其他的属性。
下面是生成的数据库脚本:
Here's what the SQL dumps will look like for our Posts HABTM Tags example:
--
-- Table structure for table `posts`
--
CREATE TABLE `posts` (
`id` int(10) unsigned NOT NULL auto_increment,
`user_id` int(10) default NULL,
`title` varchar(50) default NULL,
`body` text,
`created` datetime default NULL,
`modified` datetime default NULL,
`status` tinyint(1) NOT NULL default '0',
PRIMARY KEY (`id`)
) TYPE=MyISAM;
-- --------------------------------------------------------
--
-- Table structure for table `posts_tags`
--
CREATE TABLE `posts_tags` (
`post_id` int(10) unsigned NOT NULL default '0',
`tag_id` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`post_id`,`tag_id`)
) TYPE=MyISAM;
-- --------------------------------------------------------
--
-- Table structure for table `tags`
--
CREATE TABLE `tags` (
`id` int(10) unsigned NOT NULL auto_increment,
`tag` varchar(100) default NULL,
PRIMARY KEY (`id`)
) TYPE=MyISAM;With our tables set up, let's define the association in the Post model:
/app/models/post.php hasAndBelongsToMany
<?php
class Post extends AppModel
{
var $name = 'Post';
var $hasAndBelongsToMany = array('Tag' =>
array('className' => 'Tag',
'joinTable' => 'posts_tags',
'foreignKey' => 'post_id',
'associationForeignKey'=> 'tag_id',
'conditions' => '',
'order' => '',
'limit' => '',
'uniq' => true,
'finderQuery' => '',
'deleteQuery' => '',
)
);
}
?>
$hasAndBelongsToMany array是定义HABTM关联的变量,简单介绍一下需要定义的key:
- className (required):关联对象类名。
- joinTable:如果你没有遵循Cake的命名约定建立关联表的话,则需要设置该key来指定关联表。
- foreignKey:注意和associationForeignKey的区别,这个是定义本model在关联表中的外键字段。当然也是仅在你没有遵循Cake命名约定的时候才需要。
- associationForeignKey:关联表中指向关联对象的外键字段名。
- conditions:关联对象限定条件。
- order: 关联对象排序子句。
- limit:关联对象检索数量限制。
- uniq:设为true的话,重复的关联对象将被过滤掉。
- finderQuery:完整的关联对象检索语句。
- deleteQuery:完整的删除关联关系的sql语句。当你需要自己实现删除操作的时候可以使用该值。
- 最后我们来看一下代码:
- $post = $this->Post->read(null, '2');
- print_r($post);
- //output:
- Array
- (
- [Post] => Array
- (
- [id] => 2
- [user_id] => 25
- [title] => Cake Model Associations
- [body] => Time saving, easy, and powerful.
- [created] => 2006-04-15 09:33:24
- [modified] => 2006-04-15 09:33:24
- [status] => 1
- )
- [Tag] => Array
- (
- [0] => Array
- (
- [id] => 247
- [tag] => CakePHP
- )
- [1] => Array
- (
- [id] => 256
- [tag] => Powerful Software
- )
- )
- )
保存关联对象
请记住一件非常重要的事情,当保存对象时,很多时候需要同时保存关联对象,比如当我们保存Post对象和它关联的Comment对象时,我们会同时用到Post和Comment两个model的操作。
抽象出来说,当关联的两个对象都没有持久化(即未保存在数据库中),你需要首先持久化主对象,或者是父对象。我们通过保存Post和关联的一条Comment这个场景来具体看看是如何操作的:
- //------------Post Comment都没有持久化------------
- /app/controllers/posts_controller.php (partial)
- function add()
- {
- if (!emptyempty($this->data))
- {
- //We can save the Post data:
- //it should be in $this->data['Post']
- $this->Post->save($this->data);
- //Now, we'll need to save the Comment data
- //But first, we need to know the ID for the
- //Post we just saved...
- $post_id = $this->Post->getLastInsertId();
- //Now we add this information to the save data
- //and save the comment.
- $this->data['Comment']['post_id'] = $post_id;
- //Because our Post hasMany Comments, we can access
- //the Comment model through the Post model:
- $this->Post->Comment->save($this->data);
- }
- }
换一种情形,假设为现有的一篇Post添加一个新的Comment记录,你需要知道父对象的ID。你可以通过URL来传递这个参数或者使用一个Hidden字段来提交。
- /app/controllers/posts_controller.php (partial)
- //Here's how it would look if the URL param is used...
- function addComment($post_id)
- {
- if (!emptyempty($this->data))
- {
- //You might want to make the $post_id data more safe,
- //but this will suffice for a working example..
- $this->data['Comment']['post_id'] = $post_id;
- //Because our Post hasMany Comments, we can access
- //the Comment model through the Post model:
- $this->Post->Comment->save($this->data);
- }
- }
如果你使用hidden字段来提交ID这个参数,你需要对这个隐藏元素命名(如果你使用HtmlHelper)来正确提交:
假设日志的ID我们这样来命名$post['Post']['id']
<?php echo $html->hidden('Comment/post_id', array('value' => $post['Post']['id'])); ?>
这样来命名的话,Post对象的ID可以通过$this->data['Comment']['post_id']来访问,同样的通过$this->Post->Comment->save($this->data)也能非常简单的调用。
当保存多个子对象时,采用一样的方法,只需要在一个循环中调用save()方法就可以了(但是要记住使用Model::create()方法来初始化对象)。
小结一下,无论是belongsTo, hasOne还是hasMany关联,在保存关联子对象时候都要记住把父对象的ID保存在子对象中。
保存 hasAndBelongsToMany 关联对象
如 果定义关联一样,最复杂的莫过于 hasAndBelongsToMany 关联,hasOne, belongsTo, hasMany这3种关联只需要很简单的保存一下关联对象外键ID就可以了。但是 hasAndBelongsToMany 却没有那么容易了,不过我们也做了些努力,使之尽可能变得简单些。继续我们的Blog的例子,我们需要保存一个Post,并且关联一些Tag。
实际项目中你需要有一个单独的form来创建新的tag然后来关联它们,不过为了叙述简单,我们假定已经创建完毕了,只介绍如何关联它们的动作。
当我们在Cake中保存一个model,页面上tag的名字(假设你使用了HtmlHelper)应该是这样的格式 'Model/field_name' 。好了,让我们开始看页面代码:
/app/views/posts/add.thtml Form for creating posts
<h1>Write a New Post</h1>
<table>
<tr>
<td>Title:</td>
<td><?php echo $html->input('Post/title')?></td>
</tr>
<tr>
<td>Body:<td>
<td><?php echo $html->textarea('Post/title')?></td>
</tr>
<tr>
<td colspan="2">
<?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
<?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
<?php echo $html->submit('Save Post')?>
</td>
</tr>
</table>
上述页面仅仅创建了一个Post记录,我们还需要加入些代码来关联tag:
/app/views/posts/add.thtml (Tag association code added)
<h1>Write a New Post</h1>
<table>
<tr>
<td>Title:</td>
<td><?php echo $html->input('Post/title')?></td>
</tr>
<tr>
<td>Body:</td>
<td><?php echo $html->textarea('Post/title')?></td>
</tr>
<tr>
<td>Related Tags:</td>
<td><?php echo $html->selectTag('Tag/Tag', $tags, null, array('multiple' => 'multiple')) ?>
</td>
</tr>
<tr>
<td colspan="2">
<?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
<?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
<?php echo $html->submit('Save Post')?>
</td>
</tr>
</table>
我 们在controller中通过调用 $this->Post->save() 来保存当前Post以及关联的tag信息,页面元素的命名必须是这样的格式 "Tag/Tag" (Cake Tag Render之后实际的Html Tag名字为 'data[ModelName][ModelName][]' 这样的格式)。提交的数据必须是单个ID,或者是一个ID的array。因为我们使用了一个复选框,所以这里提交的是一个ID的array。
$tags变量是一个array,包含了复选框所需要的tag的ID以及Name信息。
使用 bindModel() 和 unbindModel() 实时地改变关联关系
有的时候可能你会需要实时地,动态的改变model的关联关系,比如在一个异常情况下。特别是LazyLoad的问题,有的时候我们并需要一个完整的model,所以我们可以使用 bindModel() 和 unbindModel()来绑定或者解除绑定关联对象。
代码说话,Start:
- leader.php and follower.php
- <?php
- class Leader extends AppModel
- {
- var $name = 'Leader';
- var $hasMany = array(
- 'Follower' => array(
- 'className' => 'Follower',
- 'order' => 'Follower.rank'
- )
- );
- }
- ?>
- <?php
- class Follower extends AppModel
- {
- var $name = 'Follower';
- }
- ?>
上述两个Model,在Leader Model中,有一个hasMany关联,定义了 "Leader hasMany Followers" 这样的关系。下面我们演示如何在Controller中动态地解除这种关联绑定。
- leaders_controller.php (partial)
- function someAction()
- {
- //This fetches Leaders, and their associated Followers
- $this->Leader->findAll();
- //Let's remove the hasMany...
- $this->Leader->unbindModel(array('hasMany' => array('Follower')));
- //Now a using a find function will return Leaders, with no Followers
- $this->Leader->findAll();
- //NOTE: unbindModel only affects the very next find function.
- //注意:unbindModel方法只作用一次,第二次find方法调用时则仍然是关联关系有效的
- //An additional find call will use the configured association information.
- //We've already used findAll() after unbindModel(), so this will fetch
- //Leaders with associated Followers once again...
- $this->Leader->findAll();
- }
对于其他各种关联的unbindModel()的用法是类似的,你只需要更改名字和类型就可以了,下面介绍一些基础的Usage:
通用的unbindModel()
$this->Model->unbindModel(array('associationType' => array('associatedModelClassName')));
掌握了如何动态的解除绑定之后,让我们看看如何动态的绑定关联关系。
- leaders_controller.php (partial)
- funciton anotherAction()
- {
- //There is no Leader hasMany Principles in the leader.php model file, so
- //a find here, only fetches Leaders.
- $this->Leader->findAll();
- //Let's use bindModel() to add a new association to the Principle model:
- $this->Leader->bindModel(
- array('hasMany' => array(
- 'Principle' => array(
- 'className' => 'Principle'
- )
- )
- )
- );
- //Now that we're associated correctly, we can use a single find function
- //to fetch Leaders with their associated principles:
- $this->Leader->findAll();
- }
bindModel()方法不单能创建一个关联,同样可以用来动态的修改一个关联。
- 下面是通常的用法:
- Generic bindModel() example
- $this->Model->bindModel(
- array('associationName' => array(
- 'associatedModelClassName' => array(
- // normal association keys go here...
- )
- )
- )
- );
注意:这些的前提是你的数据库表中的外键关联等已经正确设置。