Одной из самых мощных особенностей CakePHP является способность создавать реляционные связи, предоставляемая моделью. В CakePHP, связи между моделями осуществляются с помощью ассоциаций.
Определение связей между различными объектами в вашем приложении должно быть естественным процессом. Например: рецепт может иметь много оценок, оценки принадлежат (поставлены) авторами, и авторы могут иметь много рецептов. Определив каким образом эти связи будут работать, вы легко сможете получить доступ к вашим данным.
Цель этого раздела – показать как определять и использовать ассоциации между моделями в CakePHP.
Данные могут получаться из разных источников. Наиболее распространенная форма хранения в веб-приложениях – это реляционная база данных. Об этом и пойдет речь в этом разделе.
В CakePHP существует четыре типа ассоциаций: hasOne, hasMany, belongsTo, hasAndBelongsToMany (HABTM).
Связь | Тип ассоциации | Пример |
один к одному | hasOne | Пользователь имеет один профиль |
один ко многим | hasMany | Пользователь имеет много рецептов |
многие к одному | belongsTo | Рецепты принадлежат пользователю |
многие ко многим | hasAndBelongsToMany | Рецепты имеют и принадлежат множеству тегов |
Ассоциации определяются созданием переменной класса, имя которой совпадает с названием, определяемой вами, ассоциации. Эта переменная может быть простой, как строка, а может быть сложной, как многомерный массив, используемый для определения особенностей ассоциации.
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = 'Profile';
var $hasMany = array(
'Recipe' => array(
'className' => 'Recipe',
'conditions' => array('Recipe.approved' => '1'),
'order' => 'Recipe.created DESC'
)
);
}
?>
В приведенном примере в строке 'className' => 'Recipe', 'Recipe' – это алиас. Это идентификатор для связи и может быть любым. Обычно, вы будете выбирать такое же имя, которое имеет класс, на который ссылается данный класс. Алиасы должны быть уникальными как для отдельных моделей, так и с двух сторон связей belongsTo/hasMany или belongTo/hasOne. Выбор не уникальных имен для алиасов моделей, может стать причиной непредсказуемого поведения.
CakePHP будет автоматически создавать связи между ассоциируемыми объектами моделей. Таким образом, например, в вашей модели User вы можете получить доступ к модели Recipe:
<? $this->Recipe->someFunction(); ?>
Подобным образом, в вашем контроллере вы можете получить доступ к ассоциируемым моделям, без добавления их в массив $uses:
<? $this->User->Recipe->someFunction(); ?>
Помните, что ассоциации, определяются в одну сторону. Если вы определили User hasMany Recipe,
то это ничего не дает модели Recipe. Вам необходимо определить Recipe belongsTo User для того,
чтобы можно было обращаться к модели User из модели Recipe.
Давайте создадим hasOne ассоциацию модели User с моделью Profile.
Во-первых, ваши таблицы БД должны иметь корректные ключи. Для того, чтобы связь hasOne работала, одна таблица должна содержать внешний (вторичный) ключ, который указывает на запись другой таблицы. В данном случае таблица profiles будет содержать поле с именем user_id.
hasOne: другая модель содержит внешний ключ.
Связь | Схема данных |
Apple hasOne Banana | bananas.apple_id |
User hasOne Profile | profiles.user_id |
Doctor hasOne Mentor | mentors.doctor_id |
Файл модели User будет сохранен в /app/models/user.php. Для определения ассоциации 'User hasOne Profile', добавьте свойство $hasOne в класс модели. Не забудьте создать модель Profile в /app/models/profile.php, иначе ассоциация не будет работать.
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = 'Profile';
}
?>
Существует два способа для описания этой связи в вашем файле модели. Самый простой – это присвоить переменной $hasOne строку, содержащую имя класса ассоциируемой модели (как мы сделали выше).
Если вам надо больше управления, то вы можете определить вашу ассоциацию, используя синтаксис массива. Например, вы можете захотеть ограничить ассоциацию, включая, только определенные записи.
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = array(
'Profile' => array(
'className' => 'Profile',
'conditions' => array('Profile.published' => '1'),
'dependent' => true
)
);
}
?>
Возможные индексы для hasOne ассоциации:
После того, как эта ассоциация определена, find методы модели User будут также выбирать данные ассоциированной модели 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
)
)
Теперь у нас есть доступ к модели Profile из модели User, давайте определим ассоциацию belongsTo в модели Profile для того, чтобы получить доступ к данным модели User. Ассоциация belongsTo – естественное дополнение к hasOne и hasMany: она позволяет нам видеть данные в обратном направлении.
При создании ключей таблиц БД для ассоциации belongsTo следуйте следующему соглашению:
Связь | Схема данных |
Banana belongsTo Apple | bananas.apple_id |
Profile belongsTo User | profiles.user_id |
Mentor belongsTo Doctor | mentors.doctor_id |
Если модель (таблица) содержит внешний ключ, то она belongsTo другой модели (таблице).
Мы можем определить belongsTo ассоциацию в нашей модели Profile в /app/models/profile.php, используя строковый синтаксис:
<?php
class Profile extends AppModel {
var $name = 'Profile';
var $belongsTo = 'User';
}
?>
Мы так же можем определить и более специфическую связь, используя синтаксис массива:
<?php
class Profile extends AppModel {
var $name = 'Profile';
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
?>
Возможные индексы для массивов belongsTo ассоциации:
После того, как данная ассоциация определена, find операции модели Profile будут также выбирать данные из модели 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 позволит нам получать все комментарии пользователя, когда мы выбираем этого пользователя.
При создании ключей таблиц БД для ассоциации hasMany следуйте следующему соглашению:
Связь | Схема данных |
User hasMany Comment | Comment.user_id |
Cake hasMany Virtue | Virtue.cake_id |
Product hasMany Option | Option.product_id |
Мы можем определить ассоциацию hasMany в нашей модели User в /app/models/user.php, используя строковый синтаксис:
<?php
class User extends AppModel {
var $name = 'User';
var $hasMany = 'Comment';
}
?>
Мы так же можем определить и более специфическую связь, используя синтаксис массива:
<?php
class User extends AppModel {
var $name = 'User';
var $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id',
'conditions' => array('Comment.status' => '1'),
'order' => 'Comment.created DESC',
'limit' => '5',
'dependent'=> true
)
);
}
?>
Возможные индексы для массивов hasMany ассоциации:
Второй параметр метода Model->delete() должен быть установлен в true
для включения рекурсивного удаления.
Когда данная ассоциация определена, то find операции в модели User будут также выбирать записи связанной модели 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] => 123
[user_id] => 121
[title] => More on Gwoo
[body] => But what of the ‘Nut?
[created] => 2006-05-01 10:41:01
)
)
)
Помните, что вам надо определить ассоциацию 'Comment belongsTo User' для того, чтобы получать данные в обоих направлениях.
На данный момент вы можете называть себя профессионалом ассоциирования моделей в CakePHP. Вы уже хорошо разбираетесь в трех видах ассоциаций, который охватывают основную массу отношений объектов.
Давайте рассмотрим последний тип отношений: hasAndBelongsToMany, или HABTM. Эта ассоциация используется, когда у вас есть две модели, которые должны быть связаны многократно, множеством различных способов.
Главное отличие между hasMany и HABTM – это то, что связь между моделями не исключающая. Например, мы соединяем нашу модель Recipe с моделью Tag, используя HABTM. Прикрепление тега «украинский» к бабушкиному рецепту борща, не «израсходует» этот тег. При необходимости, я могу прикрепить этот тег и к другим рецептам.
Связи между объектами, ассоциированными через hasMany, – исключающие. Если User hasMany Comments, то комментарии принадлежит только определенному пользователю и не может быть привязан к другому.
Идем дальше. Нам необходимо создать дополнительную таблицу в базе данных, для управления HABTM ассоциацией. Имя этой новой соединяющей таблицы должно состоять из имен двух используемых моделей (в алфавитном порядке), разделенных символом подчеркивания ( _ ). Таблица должна содержать два поля, внешние ключи (типа integer), указывающие на первичные ключи, используемых моделей. Чтобы избежать конфликтных ситуаций – не определяйте комбинированные первичные ключи для этих двух полей. Если это, все же, необходимо, то вы можете определить уникальный индекс. Если вы планируете добавить другую дополнительную информацию в эту таблицу, то добавьте поле с первичным ключом (по соглашению 'id'), чтобы вы могли работать с этой таблицей также легко, как с другими моделями.
HABTM требует отдельную связывающую таблицу, которая содержит имена обоих моделей.
Связь | Схема данных |
Recipe HABTM Tag | id, recipes_tags.recipe_id, recipes_tags.tag_id |
Cake HABTM Fan | id, cakes_fans.cake_id, cakes_fans.fan_id |
Foo HABTM Bar | id, bars_foos.foo_id, bars_foos.bar_id |
Имя таблицы, по соглашению, в алфавитном порядке.
После того, как эта новая таблица создана, мы можем определить HABTM ассоциацию в файлах моделей. В этот раз мы пропустим строковый синтаксис и перейдем сразу к определению ассоциации с помощью массива:
<?php
class Recipe extends AppModel {
var $name = 'Recipe';
var $hasAndBelongsToMany = array(
'Tag' =>
array(
'className' => 'Tag',
'joinTable' => 'recipes_tags',
'with' => '',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'tag_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
?>
Возможные индексы для массивов HABTM ассоциаций:
После того, как эта ассоциация определена, find операции модели Recipe будут также выбирать записи связанной модели Tag, если такие существуют:
//Пример результатов после вызова $this->Recipe->find().
Array
(
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Breakfast
)
[1] => Array
(
[id] => 124
[name] => Dessert
)
[2] => Array
(
[id] => 125
[name] => Heart Disease
)
)
)
Помните, что надо определить HABTM ассоциацию в модели Tag, если вы пожелаете выбирать данные модели Recipe из модели Tag.
Также возможно выполнять пользовательские find запросы, на основе HABTM отношений. Рассмотрим следующий пример:
Возьмем ту же структуру, как в примере выше (Recipe HABTM Tag). Допустим, мы хотим выбрать все рецепты с тегом 'десерт'. Один из вариантов (неверный) – применить условие непосредственно в определении ассоциации:
<?
$this->Recipe->bindModel(array(
'hasAndBelongsToMany' => array(
'Tag' => array('conditions'=>array('Tag.name'=>'десерт'))
)));
$this->Recipe->find('all');
?>
//полученные данные
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Шоколад сахарные бомбы
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 124
[name] => десерт
)
)
)
1 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => крабовые пирожные
[created] => 2008-05-01 10:31:01
[user_id] => 2349
)
[Tag] => Array
(
}
}
}
Обратите внимание, что в данном случае были возвращены все рецепты, но только теги «десерт». Есть несколько способов достижения нашей цели. Первый – это искать в модели Tag (вместо модели Recipe), это также даст нам все ассоциированные рецепты.
<?
$this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'десерт')));
?>
Мы также можем использовать модель соединяющей таблицы (которую CakePHP предоставляет нам), для поиска по данному ID.
<?
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions'=>array('RecipesTag.tag_id'=>124) // id тега "десерт"
));
?>
Также можно создать экзотическую ассоциацию для создания множества необходимых связей, для фильтрации, например:
<?
$this->Recipe->bindModel(array(
'hasOne' => array(
'RecipesTag',
'FilterTag' => array(
'className' => 'Tag',
'foreignKey' => false,
'conditions' => array('FilterTag.id = RecipesTag.id')
))));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions'=>array('FilterTag.name'=>'десерт')
));
?>
//полученные данные
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Breakfast
)
[1] => Array
(
[id] => 124
[name] => Dessert
)
[2] => Array
(
[id] => 125
[name] => Heart Disease
)
)
}
?>
Такие же трюки связывания могут быть использованы для нумерации ваших HABTM моделей. Единственное предупреждение: нумерация требует два запроса (один для подсчета количества записей, а другой – для получения данных). Передавайте параметр false в bindModel(); который скажет CakePHP сохранять связь для множества запросов, а не для одного (по умолчанию). Обращайтесь к API за дополнительной информацией.
За дополнительной информацией по связыванию моделей на лету, смотрите 'Создание и уничтожение ассоциаций на лету.'
Иногда необходимо создать и уничтожить ассоциацию моделей на лету. Это может понадобиться по различным причинам:
Создание и удаление этих ассоциаций производится с помощью методов bindModel() и unbindModel(). (Также есть очень полезное поведение Containable, смотрите раздел руководства о встроенных поведениях, для дополнительной информации.). Давайте создадим несколько моделей, чтобы мы могли увидеть как работают bindModel() и unbindModel(). Мы начнем с двух моделей:
<?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';
}
?>
Теперь, в контроллере Leaders Controller?, мы можем использовать метод find() в модели Leader, для получения лидера и всех ассоциированных к нему последователей. Как вы могли видеть выше – ассоциативный массив в модели Leader определяет связь Leader hasMany Followers. В качестве демонстрации, давайте используем unbindModel() для удаления ассоциации в действии контроллера.
<?
function someAction() {
// Это выбирает лидера и его ассоциированных последователей
$this->Leader->findAll();
// Давайте удалим hasMany...
$this->Leader->unbindModel(
array('hasMany' => array('Follower'))
);
// Теперь функция find вернет
// Лидеров, без последователей
$this->Leader->findAll();
// Внимание: unbindModel влияет только на следующую
// функцию find. Дополнительный вызов find call будет
// использовать заданную информацию об ассоциации.
// Мы уже использовали findAll() после unbindModel(),
// так что, этот вызов снова вернет лидеров с
// последователями...
$this->Leader->findAll();
}
?>
Удаление или добавление ассоциаций с использованием bind- и unbindModel() работает только для следующей операции модели, пока второй параметр не установлен в false. Если второй параметр установлен в false, то bind останется и для других запросов.
Здесь базовый пример использования unbindModel():
<?
$this->Model->unbindModel(
array('associationType' => array('associatedModelClassName'))
);
?>
Теперь, когда мы успешно удалили ассоциацию на лету, давайте добавим её. Наши, пока что, безпринципные лидеры нуждаются в ассоциированных принципах. Файл модели Principle – пустой, за исключением выражения var $name. Давайте привяжем нашим лидерам некоторые принципы на лету (но помните, что только для следующей find операции). Эта функция появляется в Leaders Controller?:
<?
function anotherAction() {
// в файле leader.php нет Leader hasMany Principles
// таким образом, find выберет только лидеров
$this->Leader->findAll();
// Используем bindModel() для добавления новой
// ассоциации в модель Leader:
$this->Leader->bindModel(
array('hasMany' => array(
'Principle' => array(
'className' => 'Principle'
)
)
)
);
// Теперь мы можем использовать одну find функцию для
// получения лидеров и их принципов:
$this->Leader->findAll();
}
?>
The basic usage for bindModel() is the encapsulation of a normal association array inside an array whose key is named after the type of association you are trying to create:
<?
$this->Model->bindModel(
array('associationName' => array(
'associatedModelClassName' => array(
// normal association keys go here...
)
)
)
);
?>