Связи между таблицами
Таблицы редко живут отдельно: у статьи есть автор и комментарии. Связь — описание того, как записи одной таблицы относятся к записям другой. Описав связь один раз, связанные данные получают одним вызовом.
Подготовка
Пусть комментарии хранятся в таблице comments со столбцами id, article_id, body, created. Создайте её миграцией и сгенерируйте модель:
bin/cake bake migration CreateComments article_id:integer body:text created
bin/cake migrations migrate
bin/cake bake model CommentsСтолбец article_id — внешний ключ: в нём лежит номер статьи, к которой относится комментарий.
Описание связи
Связи записывают в методе initialize() класса таблицы:
// src/Model/Table/ArticlesTable.php
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
$this->hasMany('Comments');
}hasMany('Comments') означает: у статьи много комментариев. Имя столбца article_id ORM выводит из соглашений. Если модель создавалась командой bake model уже после появления внешнего ключа, генератор добавит связи сам.
Виды связей
| Связь | Что означает | Где внешний ключ |
|---|---|---|
hasMany('Comments') | У статьи много комментариев | comments.article_id |
belongsTo('Articles') | Комментарий относится к статье | comments.article_id |
hasOne('Profiles') | У пользователя один профиль | profiles.user_id |
belongsToMany('Tags') | У статьи много меток, у метки много статей | Таблица articles_tags |
hasMany и belongsTo — две стороны одной связи: первую пишут в ArticlesTable, вторую — в CommentsTable.
Загрузка связанных записей
// src/Controller/ArticlesController.php
public function view(int $id): void
{
$article = $this->Articles->get($id, contain: ['Comments']);
$this->set(compact('article'));
}Параметр contain перечисляет связи, которые нужно загрузить вместе со статьёй.
<h1><?= h($article->title) ?></h1>
<?php foreach ($article->comments as $comment): ?>
<p><?= h($comment->body) ?></p>
<?php endforeach; ?>Комментарии лежат в свойстве comments — имя связи строчными буквами. Для связи с одной записью свойство в единственном числе: $comment->article.
В запросе find
$articles = $this->Articles->find()
->contain(['Comments'])
->all();Сколько бы статей ни нашлось, комментарии загрузятся одним дополнительным запросом.
Отбор по связанной таблице
$articles = $this->Articles->find()
->matching('Comments', fn ($q) => $q->where(['Comments.body LIKE' => '%спасибо%']))
->all();matching() оставляет только статьи, у которых есть подходящий комментарий.
Сохранение связанной записи
$comment = $this->Articles->Comments->newEntity([
'article_id' => $article->id,
'body' => 'Отличная статья!',
]);
$this->Articles->Comments->save($comment);Связанная таблица доступна через связь: $this->Articles->Comments. Чтобы newEntity() принял article_id, поле должно быть в $_accessible класса Comment.
Частая ошибка
Обращение к $article->comments без contain даёт null, а не комментарии: CakePHP не выполняет запросы «по требованию». Это защищает от сотен скрытых запросов, но связи нужно перечислять явно. Если связанное свойство пустое, первым делом проверьте contain.