在你的应用程序中定义不同对象之间的关系应该是一个自然的过程。例如,一篇文章可能有多个评论,并且属于一个作者。作者可能有多篇文章和评论。CakePHP 中的四种关联类型是:hasOne、hasMany、belongsTo 和 belongsToMany。
关系 |
关联类型 |
示例 |
---|---|---|
一对一 |
hasOne |
一个用户有一个配置文件。 |
一对多 |
hasMany |
一个用户可以有多篇文章。 |
多对一 |
belongsTo |
多篇文章属于一个用户。 |
多对多 |
belongsToMany |
标签属于多篇文章。 |
关联是在你的表格对象的 initialize()
方法中定义的。与关联类型匹配的方法允许你在你的应用程序中定义关联。例如,如果我们想在我们的 ArticlesTable 中定义一个 belongsTo 关联
namespace App\Model\Table;
use Cake\ORM\Table;
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Authors');
}
}
任何关联设置的最简单形式都接受你想要关联的表格别名。默认情况下,所有关联的细节都将使用 CakePHP 约定。如果你想自定义你的关联的处理方式,你可以使用设置器修改它们
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Authors', [
'className' => 'Publishing.Authors'
])
->setForeignKey('author_id')
->setProperty('author');
}
}
属性名称将是实体对象上的关联实体的属性键,在本例中为
$authorEntity = $articleEntity->author;
你也可以使用数组来自定义你的关联
$this->belongsTo('Authors', [
'className' => 'Publishing.Authors',
'foreignKey' => 'author_id',
'propertyName' => 'author'
]);
但是,数组不提供流式接口所提供的类型提示和自动完成功能。
同一张表可以被多次使用来定义不同类型的关联。例如,考虑你想要将已批准的评论和尚未被审核的评论分开的情况
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->hasMany('Comments')
->setFinder('approved');
$this->hasMany('UnapprovedComments', [
'className' => 'Comments'
])
->setFinder('unapproved')
->setProperty('unapproved_comments');
}
}
如你所见,通过指定 className
键,可以将同一张表用作同一张表的不同关联。你甚至可以创建自关联的表来创建父子关系
class CategoriesTable extends Table
{
public function initialize(array $config): void
{
$this->hasMany('SubCategories', [
'className' => 'Categories',
]);
$this->belongsTo('ParentCategories', [
'className' => 'Categories',
]);
}
}
你也可以通过对 Table::addAssociations()
进行一次调用来批量设置关联,它接受一个包含以关联类型为索引的表格名称集的数组作为参数
class PostsTable extends Table
{
public function initialize(array $config): void
{
$this->addAssociations([
'belongsTo' => [
'Users' => ['className' => 'App\Model\Table\UsersTable'],
],
'hasMany' => ['Comments'],
'belongsToMany' => ['Tags'],
]);
}
}
每个关联类型都接受多个关联,其中键是别名,值是关联配置数据。如果使用数字键,则值将被视为关联别名。
让我们设置一个 Users 表格,它与 Addresses 表格有一个 hasOne 关系。
首先,你的数据库表格需要正确地加键。为了使 hasOne 关系起作用,一个表格必须包含指向另一个表格中记录的外部键。在本例中,Addresses 表格将包含一个名为 ‘user_id’ 的字段。基本模式是
hasOne: 另一个 模型包含外部键。
关系 |
模式 |
---|---|
Users hasOne Addresses |
addresses.user_id |
Doctors hasOne Mentors |
mentors.doctor_id |
注意
不必强制遵循 CakePHP 约定,你可以在你的关联定义中覆盖任何 foreignKey
的名称。但是,坚持约定会使你的代码重复性更少,更易于阅读和维护。
创建 UsersTable
和 AddressesTable
类后,可以使用以下代码进行关联
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->hasOne('Addresses');
}
}
如果你需要更多控制,可以使用设置器定义你的关联。例如,你可能想要限制关联以仅包含某些记录
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->hasOne('Addresses')
->setName('Addresses')
->setFinder('primary')
->setDependent(true);
}
}
如果你想将不同的地址分解成多个关联,你可以执行类似的操作
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->hasOne('HomeAddresses', [
'className' => 'Addresses'
])
->setProperty('home_address')
->setConditions(['HomeAddresses.label' => 'Home'])
->setDependent(true);
$this->hasOne('WorkAddresses', [
'className' => 'Addresses'
])
->setProperty('work_address')
->setConditions(['WorkAddresses.label' => 'Work'])
->setDependent(true);
}
}
注意
如果多个 hasOne 关联共享一个列,则必须使用关联别名限定它。在上面的示例中,’label’ 列使用 ‘HomeAddresses’ 和 ‘WorkAddresses’ 别名限定。
HasOne 关联数组的可能键包括
className: 另一个表格的类名。这是在获取表格实例时使用的相同名称。在 ‘Users hasOne Addresses’ 示例中,它应该是 ‘Addresses’。默认值为关联的名称。
foreignKey: 另一个表格中外部键列的名称。默认值为当前模型的带下划线的单数名称,后缀为 ‘_id’,例如上面的示例中的 ‘user_id’。
bindingKey: 当前表格中用于匹配 foreignKey
的列的名称。默认值为当前表格的主键,例如上面示例中 Users 的 ‘id’。
conditions: 一个包含 find() 兼容条件的数组,例如 ['Addresses.primary' => true]
joinType: SQL 查询中使用的连接类型。接受的值是 ‘LEFT’ 和 ‘INNER’。你可以使用 ‘INNER’ 仅获取设置了关联的结果。默认值为 ‘LEFT’。
dependent: 当 dependent 键设置为 true
且一个实体被删除时,关联的模型记录也会被删除。在本例中,我们将它设置为 true
,以便删除用户也会删除她关联的地址。
cascadeCallbacks: 当这个键和 dependent 键都为 true
时,级联删除将加载和删除实体,以便正确触发回调。当 false
时,deleteAll()
用于删除关联的数据,并且不会触发任何回调。
propertyName: 应该用关联表格中的数据填充到源表格结果中的属性名称。默认情况下,这是关联的带下划线和单数的名称,因此在我们的示例中为 address
。
strategy: 用于从另一个表格加载匹配记录的查询策略。接受的值是 'join'
和 'select'
。使用 'select'
将生成一个单独的查询,当另一个表格位于不同的数据库中时非常有用。默认值为 'join'
。
finder: 用于加载关联记录的查找器方法。
定义了此关联后,对 Users 表格的查找操作可以包含地址记录(如果存在)
// In a controller or table method.
$query = $users->find('all')->contain(['Addresses'])->all();
foreach ($query as $user) {
echo $user->address->street;
}
以上将发出类似于以下内容的 SQL
SELECT * FROM users INNER JOIN addresses ON addresses.user_id = users.id;
现在我们已经可以从 User 表格访问 Address 数据,让我们在 Addresses 表格中定义一个 belongsTo 关联,以便访问相关的 User 数据。belongsTo 关联是 hasOne 和 hasMany 关联的自然补充 - 它允许我们从另一个方向查看相关数据。
在为 belongsTo 关系对数据库表格加键时,请遵循以下约定
belongsTo: 当前 模型包含外部键。
关系 |
模式 |
---|---|
Addresses belongsTo Users |
addresses.user_id |
Mentors belongsTo Doctors |
mentors.doctor_id |
提示
如果一个表格包含一个外部键,它就属于另一个表格。
我们可以如下在 Addresses 表格中定义 belongsTo 关联
class AddressesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Users');
}
}
我们也可以使用设置器定义更具体的关联
class AddressesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Users')
->setForeignKey('user_id')
->setJoinType('INNER');
}
}
belongsTo 关联数组的可能键包括
className: 另一个表格的类名。这是在获取表格实例时使用的相同名称。在 ‘Addresses belongsTo Users’ 示例中,它应该是 ‘Users’。默认值为关联的名称。
foreignKey: 当前表格中外部键列的名称。默认值为另一个模型的带下划线的单数名称,后缀为 ‘_id’,例如上面的示例中的 ‘user_id’。
bindingKey: 另一个表格中用于匹配 foreignKey
的列的名称。默认值为另一个表格的主键,例如上面示例中 Users 的 ‘id’。
conditions: 一个包含 find() 兼容条件或 SQL 字符串的数组,例如 ['Users.active' => true]
joinType: SQL 查询中使用的连接类型。接受的值是 ‘LEFT’ 和 ‘INNER’。你可以使用 ‘INNER’ 仅获取设置了关联的结果。默认值为 ‘LEFT’。
propertyName: 应该用关联表格中的数据填充到源表格结果中的属性名称。默认情况下,这是关联的带下划线和单数的名称,因此在我们的示例中为 user
。
strategy: 用于从另一个表格加载匹配记录的查询策略。接受的值是 'join'
和 'select'
。使用 'select'
将生成一个单独的查询,当另一个表格位于不同的数据库中时非常有用。默认值为 'join'
。
finder: 用于加载关联记录的查找器方法。
定义了此关联后,对 Addresses 表格的查找操作可以包含 User 记录(如果存在)
// In a controller or table method.
$query = $addresses->find('all')->contain(['Users'])->all();
foreach ($query as $address) {
echo $address->user->username;
}
以上将输出类似于以下内容的 SQL
SELECT * FROM addresses LEFT JOIN users ON addresses.user_id = users.id;
hasMany 关联的一个例子是“Articles hasMany Comments”。定义这种关联后,我们可以在加载文章时获取该文章的评论。
在为 hasMany 关系创建数据库表时,请遵循以下约定
hasMany: 另一个模型包含外键。
关系 |
模式 |
---|---|
Articles hasMany Comments |
Comments.article_id |
Products hasMany Options |
Options.product_id |
Doctors hasMany Patients |
Patients.doctor_id |
我们可以在 Articles 模型中定义 hasMany 关联,如下所示
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->hasMany('Comments');
}
}
我们也可以使用设置器定义更具体的关联
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->hasMany('Comments')
->setForeignKey('article_id')
->setDependent(true);
}
}
有时您可能想要在关联中配置组合键
// Within ArticlesTable::initialize() call
$this->hasMany('Comments')
->setForeignKey([
'article_id',
'article_hash',
]);
根据上面的例子,我们已经将包含所需组合键的数组传递给 setForeignKey()
。默认情况下,bindingKey
会自动定义为 id
和 hash
,但假设您需要指定与默认值不同的绑定字段。您可以使用 setBindingKey()
手动设置。
// Within ArticlesTable::initialize() call
$this->hasMany('Comments')
->setForeignKey([
'article_id',
'article_hash',
])
->setBindingKey([
'whatever_id',
'whatever_hash',
]);
与 hasOne 关联一样,foreignKey
在另一个 (Comments) 表中,而 bindingKey
在当前 (Articles) 表中。
hasMany 关联数组中的可能键包括
className: 另一个表的类名。这与获取表实例时使用的名称相同。在“Articles hasMany Comments”示例中,它应该是“Comments”。默认值为关联的名称。
foreignKey: 另一个表中外键列的名称。默认值为当前模型的带下划线单数名称,后缀为“_id”,例如上面的示例中的“article_id”。
bindingKey: 当前表中用于匹配 foreignKey
的列的名称。默认值为当前表的主键,例如上面示例中 Articles 的“id”。
conditions: 一个包含 find() 兼容条件或 SQL 字符串的数组,例如 ['Comments.visible' => true]
。建议使用 finder
选项。
sort: 一个包含 find() 兼容排序子句或 SQL 字符串的数组,例如 ['Comments.created' => 'ASC']
dependent: 当 dependent 设置为 true
时,可以进行递归模型删除。在本例中,当其关联的 Article 记录被删除时,Comment 记录将被删除。
cascadeCallbacks: 当这个键和 dependent 键都为 true
时,级联删除将加载和删除实体,以便正确触发回调。当 false
时,deleteAll()
用于删除关联的数据,并且不会触发任何回调。
propertyName: 应该用关联表中的数据填充到源表结果中的属性名称。默认情况下,这是关联的带下划线复数名称,所以我们示例中的 comments
。
strategy: 定义要使用的查询策略。默认为“select”。另一个有效值为“subquery”,它用等效的子查询替换 IN
列表。
saveStrategy: “append” 或“replace”。默认为“append”。当“append”时,当前记录将附加到数据库中的任何记录。当“replace”时,当前集合中不存在的关联记录将被删除。如果外键是可为空列,或者如果 dependent
为 true,记录将成为孤儿。
finder: 加载关联记录时要使用的查找器方法。有关更多信息,请参见 使用关联查找器 部分。
一旦这种关联被定义,Articles 表上的查找操作可以包含 Comment 记录(如果存在)。
// In a controller or table method.
$query = $articles->find('all')->contain(['Comments'])->all();
foreach ($query as $article) {
echo $article->comments[0]->text;
}
以上将输出类似于以下内容的 SQL
SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);
当使用子查询策略时,将生成类似于以下内容的 SQL
SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (SELECT id FROM articles);
您可能希望缓存 hasMany 关联的计数。当您经常需要显示关联记录的数量,但不想加载所有记录只是为了计数时,这很有用。例如,任何给定文章的评论计数通常被缓存,以使生成文章列表更有效。您可以使用 CounterCacheBehavior 来缓存关联记录的计数。
您应该确保您的数据库表不包含与关联属性名称匹配的列。例如,如果您有与关联属性冲突的计数器字段,您必须重命名关联属性或列名。
BelongsToMany 关联的一个例子是“Article BelongsToMany Tags”,其中一篇文章的标签与其他文章共享。BelongsToMany 通常被称为“has and belongs to many”,是一种经典的“多对多”关联。
hasMany 和 BelongsToMany 之间的主要区别在于,BelongsToMany 关联中模型之间的链接不是独占的。例如,我们将 Articles 表与 Tags 表连接起来。在文章中使用“funny”作为标签,并不会“用完”这个标签。我也可以在下一篇文章中使用它。
BelongsToMany 关联需要三个数据库表。在上面的示例中,我们需要 articles
、tags
和 articles_tags
的表。articles_tags
表包含将标签和文章链接在一起的数据。连接表以涉及的两个表命名,并以下划线分隔,这是惯例。在最简单的形式中,该表包含 article_id
和 tag_id
,以及跨越两列的多列 PRIMARY KEY
索引。
belongsToMany 需要一个单独的连接表,该表包含两个模型名称。
关系 |
连接表字段 |
---|---|
Articles belongsToMany Tags |
articles_tags.id, articles_tags.tag_id, articles_tags.article_id |
Patients belongsToMany Doctors |
doctors_patients.id, doctors_patients.doctor_id, doctors_patients.patient_id. |
我们可以在我们的两个模型中定义 belongsToMany 关联,如下所示
// In src/Model/Table/ArticlesTable.php
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('Tags');
}
}
// In src/Model/Table/TagsTable.php
class TagsTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('Articles');
}
}
我们还可以使用配置定义更具体的关联
// In src/Model/Table/TagsTable.php
class TagsTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('Articles', [
'joinTable' => 'articles_tags',
]);
}
}
belongsToMany 关联数组中可能的键包括
className: 另一个表的类名。这与获取表实例时使用的名称相同。在“Articles belongsToMany Tags”示例中,它应该是“Tags”。默认值为关联的名称。
joinTable: 此关联中使用的连接表的名称(如果当前表不符合 belongsToMany 连接表的命名约定)。默认情况下,将使用此表名来加载连接表的 Table 实例。
foreignKey: 连接表上引用当前模型的外键的名称,或者在组合外键的情况下为列表。这在您需要定义多个 belongsToMany 关系时特别有用。此键的默认值为当前模型的带下划线单数名称,后缀为“_id”。
bindingKey: 当前表中将用于匹配 foreignKey
的列的名称。默认为主键。
targetForeignKey: 连接模型上引用目标模型的外键的名称,或者在组合外键的情况下为列表。此键的默认值为目标模型的带下划线单数名称,后缀为“_id”。
conditions: 一个包含 find()
兼容条件的数组。如果您对关联表有条件,您应该使用“through”模型,并在其上定义必要的 belongsTo 关联。建议使用 finder
选项。
sort: 一个包含 find() 兼容排序子句的数组。
dependent: 当 dependent 键设置为 false
时,如果实体被删除,连接表的数据将不会被删除。
through: 允许您提供要用于连接表的 Table 实例的别名,或实例本身。这使得自定义连接表键成为可能,并允许您自定义枢轴表的行为。
cascadeCallbacks: 当此值为 true
时,级联删除将加载和删除实体,以便在连接表记录上正确触发回调。当 false
时,deleteAll()
用于删除关联数据,并且不会触发任何回调。这默认为 false
,以帮助减少开销。
propertyName: 应该用关联表中的数据填充到源表结果中的属性名称。默认情况下,这是关联的带下划线复数名称,所以我们示例中的 tags
。
strategy: 定义要使用的查询策略。默认为“select”。另一个有效值为“subquery”,它用等效的子查询替换 IN
列表。
saveStrategy: “append” 或“replace”。默认为“replace”。指示用于保存关联实体的模式。前者将只在关系的两侧创建新的链接,而后者将执行擦除和替换,以在保存时在传递的实体之间创建链接。
finder: 加载关联记录时要使用的查找器方法。有关更多信息,请参见 使用关联查找器 部分。
一旦这种关联被定义,Articles 表上的查找操作可以包含 Tag 记录(如果存在)。
// In a controller or table method.
$query = $articles->find('all')->contain(['Tags'])->all();
foreach ($query as $article) {
echo $article->tags[0]->text;
}
以上将输出类似于以下内容的 SQL
SELECT * FROM articles;
SELECT * FROM tags
INNER JOIN articles_tags ON (
tags.id = article_tags.tag_id
AND article_id IN (1, 2, 3, 4, 5)
);
当使用子查询策略时,将生成类似于以下内容的 SQL
SELECT * FROM articles;
SELECT * FROM tags
INNER JOIN articles_tags ON (
tags.id = article_tags.tag_id
AND article_id IN (SELECT id FROM articles)
);
如果你打算在连接/透视表中添加额外信息,或者你需要在约定之外使用连接列,你需要定义 through
选项。 through
选项让你完全控制如何创建 belongsToMany 关联。
有时在多对多关联中存储额外数据是可取的。考虑以下情况
Student BelongsToMany Course
Course BelongsToMany Student
一个学生可以选修很多课程,一个课程可以被很多学生选修。这是一个简单的多对多关联。以下表格就足够了
id | student_id | course_id
现在,如果我们想存储学生在课程上的出勤天数和他们的最终成绩,我们需要的表格应该是
id | student_id | course_id | days_attended | grade
实现我们需求的方法是使用一个 **连接模型**,也称为 **hasMany through** 关联。也就是说,关联本身就是一个模型。所以,我们可以创建一个新的模型 CoursesMemberships。看看以下模型
class StudentsTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('Courses', [
'through' => 'CoursesMemberships',
]);
}
}
class CoursesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('Students', [
'through' => 'CoursesMemberships',
]);
}
}
class CoursesMembershipsTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Students');
$this->belongsTo('Courses');
}
}
CoursesMemberships 连接表唯一地标识了给定学生在某个课程上的参与情况,以及额外的元信息。
当使用带有 through
模型的 BelongsToMany 关系的查询对象时,将关联目标表的包含和匹配条件添加到你的查询对象中。 through
表可以随后在其他条件中被引用,例如通过在你要过滤的字段前面指定 through 表名来使用 where 条件
$query = $this->find(
'list',
valueField: 'studentFirstName', order: 'students.id'
)
->contain(['Courses'])
->matching('Courses')
->where(['CoursesMemberships.grade' => 'B']);
默认情况下,关联将根据外键列加载记录。如果你想为关联定义额外的条件,可以使用 finder
。当加载关联时,ORM 将使用你的 自定义查找器 来加载、更新或删除关联的记录。使用查找器可以封装你的查询并使其更可重用。在使用查找器加载通过连接加载的关联数据 (belongsTo/hasOne) 时,有一些限制。只有查询的以下方面将应用于根查询
Where 条件。
额外连接。
包含的关联。
其他查询方面,例如选定的列、顺序、分组、having 和其他子语句,将不会应用于根查询。 _不是_ 通过连接加载的关联 (hasMany/belongsToMany) 没有上述限制,也可以使用结果格式化程序或 map/reduce 函数。
默认情况下,关联应该使用 CamelCase 样式进行配置和引用。这使得能够以以下方式链接到相关表的属性
$this->MyTableOne->MyTableTwo->find()->...;
实体上的关联属性不使用 CamelCase 约定。相反,对于像“User belongsTo Roles”这样的 hasOne/belongsTo 关系,你会得到一个 role 属性,而不是 Role 或 Roles
// A single entity (or null if not available)
$role = $user->role;
而对于另一个方向“Roles hasMany Users”,它将是
// Collection of user entities (or null if not available)
$users = $role->users;
定义完关联后,你可以在获取结果时 急切加载关联。