CakePHP 的一个非常强劲的特性就是由模型提供关系映射,通过关联来管理多个模型间的连接。
在应用程序的不同对象间定义关系是很自然的。例如:在食谱数据库,一个食谱可能有多个评论,每个评论有一个作者,每个作者可能有多个评论。 以定义这些关系的形式工作,将允许你以一种直观且强大的方式访问你的数据库。
本节的目的是展示如何在 CakePHP 中计划、定义以及利用模型间的关系。
虽然数据可能来自各种源,但在 web 应用程序中最常见的则是存储在关系数据库中。 本节将覆盖这方面的大部分内容。
关于与插件模型一起的关联的信息,请参见 插件模型。
CakePHP 的关系类型有四种: hasOne、hasMany、belongsTo 和 hasAndBelongsToMany (HABTM)。
关系 | 关联类型 | 例子 |
---|---|---|
一对多 | hasMany | 一个用户有多份食谱 |
多对一 | belongsTo | 多份食谱属于同一个用户 |
多对多 | hasAndBelongsToMany | 多份食谱有且属于多种成分 |
关联是通过创建一个由你定义的关联命名的类变量来定义的。 此变量有时候可能是简单的字符串,但也可能是用于定义关联细节的复杂的多维数组。
class User extends AppModel {
public $hasOne = 'Profile';
public $hasMany = array(
'Recipe' => array(
'className' => 'Recipe',
'conditions' => array('Recipe.approved' => '1'),
'order' => 'Recipe.created DESC'
)
);
}
在上面的例子中,第一个实例的单词 ‘Recipe’ 是别名。它是关系的唯一标识,它可以是你选择的任何东西。通常你会选择与要引用的类相同的名字。然而,每个模型的别名在应用程序中必须唯一。合适的例子有:
class User extends AppModel {
public $hasMany = array(
'MyRecipe' => array(
'className' => 'Recipe',
)
);
public $hasAndBelongsToMany => array(
'MemberOf' => array(
'className' => 'Group',
)
);
}
class Group extends AppModel {
public $hasMany = array(
'MyRecipe' => array(
'className' => 'Recipe',
)
);
public $hasAndBelongsToMany => array(
'Member' => array(
'className' => 'User',
)
);
}
但是在所有的情况下,以下代码都不工作:
class User extends AppModel {
public $hasMany = array(
'MyRecipe' => array(
'className' => 'Recipe',
)
);
public $hasAndBelongsToMany => array(
'Member' => array(
'className' => 'Group',
)
);
}
class Group extends AppModel {
public $hasMany = array(
'MyRecipe' => array(
'className' => 'Recipe',
)
);
public $hasAndBelongsToMany => array(
'Member' => array(
'className' => 'User',
)
);
}
因为在 HABTM 关联中,别名 ‘Member’ 同时指向了 User 模型(在 Group 模型中)和 Group 模型(在 User 模型中)。 在不同的模型为某个模型起不唯一的别名,可能会带来未知的行为。
Cake 能自动在关联模型对象间建立连接。所以你可以在你的 User 模型中以如下方式访问 Recipe 模型:
$this->Recipe->someFunction();
同样的,你也能在控制器中循着模型关系访问关联模型:
$this->User->Recipe->someFunction();
注解
记住,关系定义是 ‘单向的’。如果你定义了 User hasMany Recipe,对 Recipe 模型是没有影响的。你需要定义 Recipe belongsTo User才能从 Recipe 模型访问 User 模型。
让我们设置 User 模型以 hasOne 类型关联到 Profile 模型。
首先,数据库表需要有正确的主键。对于 hasOne 关系,一个表必须包含指向另一个表的记录的外键。在本例中,profiles 表将包含一个叫做 user_id 的列。基本模式是: :
hasOne: 另一个 模型包含外键。
关系 | 结构 |
---|---|
Apple hasOne Banana | bananas.apple_id |
User hasOne Profile | profiles.user_id |
Doctor hasOne Mentor | mentors.doctor_id |
注解
关于这一点,并没有强制要求遵循 CakePHP 约定,你能够很容易地在关联定义中使用任何外键来覆盖它。虽然如此,遵守规则将使你的代码更简捷,更易于阅读和维护。
User 模型文件保存为 /app/Model/User.php。为了定义‘User hasOne Profile’ 关联,需要在模型类中添加 $hasOne属性。记得要在 /app/Model/Profile.php 文件中放一个 Profile 模型,否则关联将不工作:
class User extends AppModel {
public $hasOne = 'Profile';
}
有两种途径在模型文件中描述此关系。简单的方法是设置一个包含要关联的模型的类名的字符串型属性 $hasOne,就像我们上面做的那样。
如果需要更全面的控制,可以使用数组语法定义关联。例如,你可能想要限制关联只包含某些记录。
class User extends AppModel {
public $hasOne = array(
'Profile' => array(
'className' => 'Profile',
'conditions' => array('Profile.published' => '1'),
'dependent' => true
)
);
}
hasOne 关联数组可能包含的键有: :
一旦定义了关系,User 模型上的 find 操作将匹配存在的关联 Profile 记录:
// 调用 $this->User->find() 的示例结果。
Array
(
[User] => Array
(
[id] => 121
[name] => Gwoo the Kungwoo
[created] => 2007-05-01 10:31:01
)
[Profile] => Array
(
[id] => 12
[user_id] => 121
[skill] => Baking Cakes
[created] => 2007-05-01 10:31:01
)
)
现在我们有了通过访问 User 模型获取相关 Profile 数据的办法,让我们在 Profile 模型中定义 belongsTo 关联以获取相关的 User 数据。belongsTo 关联是 hasOne 和 hasMany 关联的自然补充:它允许我们从其它途径查看数据。
在为 belongsTo 关系定义数据库表的键时,遵循如下约定:
belongsTo: 当前模型 包含外键。
关系 | 结构 |
---|---|
Banana belongsTo Apple | bananas.apple_id |
Profile belongsTo User | profiles.user_id |
Mentor belongsTo Doctor | mentors.doctor_id |
小技巧
如果一个模型(表)包含一个外键,它 belongsTo 另一个模型(表)。
我们可以使用如下字符串语法,在 /app/Model/Profile.php 文件中的 Profile 模型中定义 belongsTo 关联:
class Profile extends AppModel {
public $belongsTo = 'User';
}
我们还能使用数组语法定义特定的关系:
class Profile extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
belongsTo 关联数组可能包含的键有:
className: 被关联到当前模型的模型类名。如果你定义了 ‘Profile belongsTo User’关系,类名键的值将为 ‘User.’
foreignKey: 当前模型中需要的外键。用于需要定义多个 belongsTo 关系。其默认值为另一模型的单数模型名缀以 ‘_id’。
conditions: 一个 find() 兼容条件的数组或者类似 array('User.active' => true) 的 SQL 字符串。
type: SQL 查询的 join 类型,默认为 Left,这不可能在所有情况下都符合你的需求,在你想要从主模型和关联模型获取全部内容或者什么都不要时很有用!(仅在某些条件下有效)。 (注:类型值必须是小写,例如:left, inner)
fields: 需要在匹配的关联模型数据中获取的列的列表。默认返回所有的列。
order: 一个 find() 兼容排序子句或者类似 array('User.username' => 'ASC') 的 SQL 字符串。
counterCache: 如果此键的值设置为 true,当你在做 “save()” 或者 “delete()” 操作时关联模型将自动递增或递减外键关联的表的 “[singular_model_name]_count” 列的值。如果它是一个字符串,则其将是计数用的列名。计数列的值表示关联行的数量。也可以通过使用数组指定多个计数缓存,键为列名,值为条件,例如:
array(
'recipes_count' => true,
'recipes_published' => array('Recipe.published' => 1)
)
counterScope: 用于更新计数缓存列的可选条件数组。
一旦定义了关联,Profile 模型上的 find 操作将同时获取相关的 User 记录(如果它存在的话):
//调用 $this->Profile->find() 的示例结果。
Array
(
[Profile] => Array
(
[id] => 12
[user_id] => 121
[skill] => Baking Cakes
[created] => 2007-05-01 10:31:01
)
[User] => Array
(
[id] => 121
[name] => Gwoo the Kungwoo
[created] => 2007-05-01 10:31:01
)
)
下一步:定义一个 “User hasMany Comment” 关联。一个 hasMany 关联将允许我们在获取 User 记录的同时获取用户的评论。
在为 hasMany 关系定义数据库表的键时,遵循如下约定:
hasMany: 其它 模型包含外键。
关系 | 结构 |
---|---|
User hasMany Comment | Comment.user_id |
Cake hasMany Virtue | Virtue.cake_id |
Product hasMany Option | Option.product_id |
我们可以使用如下字符串语法,在 /app/Model/User.php 文件中的 User 模型中定义 hasMnay 关联:
class User extends AppModel {
public $hasMany = 'Comment';
}
我们还能使用数组语法定义特定的关系:
class User extends AppModel {
public $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id',
'conditions' => array('Comment.status' => '1'),
'order' => 'Comment.created DESC',
'limit' => '5',
'dependent' => true
)
);
}
hasMany 关联数组可能包含的键有:
一旦关联被建立,User 模型上的 find 操作也将获取相关的 Comment 数据(如果它存在的话):
//调用 $this->User->find() 获得的结果示例。
Array
(
[User] => Array
(
[id] => 121
[name] => Gwoo the Kungwoo
[created] => 2007-05-01 10:31:01
)
[Comment] => Array
(
[0] => Array
(
[id] => 123
[user_id] => 121
[title] => On Gwoo the Kungwoo
[body] => The Kungwooness is not so Gwooish
[created] => 2006-05-01 10:31:01
)
[1] => Array
(
[id] => 124
[user_id] => 121
[title] => More on Gwoo
[body] => But what of the ‘Nut?
[created] => 2006-05-01 10:41:01
)
)
)
有件事需要记住:你还需要定义 Comment belongsTo User 关联,用于从两个方向获取数据。 我们在这一节概述了能够使你从 User 模型获取 Comment 数据的方法。在 Comment 模型中添加 Comment belongsTo User 关系将使你能够从 Comment 模型中获取 User 数据 - 这样的链接关系才是完整的且允许从两个模型的角度获取信息流。
这个功能帮助你缓存相关数据的计数。模型通过自己追踪指向关联 $hasMany 模型的所有的添加/删除并递增/递减父模型表的专用整数列,替代手工调用 find('count') 计算记录的计数。
这个列的名称由列的单数名后缀以下划线和单词 “count” 构成:
my_model_count
如果你有一个叫 ImageComment 的模型和一个叫 Image 的模型,你需要添加一个指向 images 表的新的整数列并命名为image_comment_count。
下面是更多的示例:
模型 | 关联模型 | 示例 |
---|---|---|
User | Image | users.image_count |
Image | ImageComment | images.image_comment_count |
BlogEntry | BlogEntryComment | blog_entries.blog_entry_comment_count |
一旦你添加了计数列,就可以使用它了。通过在你的关联中添加 counterCache 键并将其值设置为 true,可以激活 counter-cache:
class ImageComment extends AppModel {
public $belongsTo = array(
'Image' => array(
'counterCache' => true,
)
);
}
自此,你每次添加或删除一个关联到 Image 的 ImageComment,image_comment_count 字段的数字都会自动调整。
你还可以指定 counterScope。它允许你指定一个简单的条件,通知模型什么时候更新(不更新)计数值,这依赖于你如何查看。
在我们的 Image 模型示例中,我们可以象下面这样指定:
class ImageComment extends AppModel {
public $belongsTo = array(
'Image' => array(
'counterCache' => true,
'counterScope' => array('Image.active' => 1) // only count if "Image" is active = 1
)
);
}