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来实现的。下面是示例代码:
<?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对象也被检索回来,非常的方便:
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关联来实现:
<?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对象也一同被检索回来。
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对象之间建立关联吧,先看下面的示例代码:
<?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对象集合
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;
设置好了表格之后,我们就要在 Post 模型中定义关联:
<?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语句。当你需要自己实现删除操作的时候可以使用该值。
最后我们来看一下代码:
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这个场景来具体看看是如何操作的:
/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字段来提交。
//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']
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' 。好了,让我们开始看页面代码:
<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'=>$_SESSION['uid']))?>
<?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
<?php echo $html->submit('Save Post')?>
</td>
</tr>
</table>
上述页面仅仅创建了一个Post记录,我们还需要加入些代码来关联tag:
<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信息。