====== Поддержка отношений вида много-ко-многим ======
===== Определение отношения =====
Рассмотрим связь вида много-ко-многим на примере двух классов - пользователь (User) и набор групп пользователей (Group), при этом пользователи могу входить в несколько групп одновременно, а группы могу содержать много пользователей.
Объекты этих классов хранятся в таблицах соответственно user и user_group, а также существует таблица для хранения информации о связи много-ко-многим user2group:
CREATE TABLE `user_group` (
`id` bigint(20) NOT NULL auto_increment,
`title` varchar(255) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL auto_increment,
`first_name` varchar(255) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user2group` (
`id` bigint(20) NOT NULL auto_increment,
`user_id` bigint(20) default NULL,
`group_id` bigint(20) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Связь много-ко-многим моделируется в базе данных как правило через дополнительную таблицу, которая хранит первичные идентификаторы связанных объектов. Как мы уже отмечали Limb поддерживает только суррогатный автоинкрементный первичный ключ id, поэтому его также придется завести в таблице связей (если кому-то показалось, что можно обойтись составным первичным ключом).
Связь много-ко-многим описывается атрибутами **$_has_many_to_many** и с левой, и с правой стороны:
class Group extends lmbActiveRecord
{
protected $_db_table_name = 'user_group';
protected $_has_many_to_many = array('users' => array('field' => 'group_id',
'foreign_field' => 'user_id',
'table' => 'user2group',
'class' => 'User'));
}
class User extends lmbActiveRecord
{
protected $_has_many_to_many = array('groups' => array('field' => 'user_id',
'foreign_field' => 'group_id',
'table' => 'user2group',
'class' => 'Group'));
}
Одно отношение **$_has_many_to_many** описывается следующими полями:
* **table** - указывает на название таблицы, где хранятся связи. Обычно эта таблица содержит только 3 поля - два внешних ключа, и еще один автоинкрементный идентификатор. Мы предпочитаем давать этой таблице название по имени двух связывающих ею таблиц, соединяя эти два имени через "2". В нашем случае мы соединяет user и group, поэтому получилось user2group.
* **field** - указывает на название поле в таблице связей, в котором хранятся идентификаторы текущего класса.
* **foreign_field** - указывает на название поля, в котором хранятся идентификаторы связываемых объектов.
* **class** - указывает на название класса связываемых объектов.
* **collection** - указывает на название класса, через который реализуется связь. По-умолчанию это значение равно lmbARManyToManyCollection.
Повторим еще раз: связи описываются с двух сторон.
===== Работа со связанными объектами =====
Использование связи много-ко-многим похожа на один-ко-многим:
$user1 = new User();
$user1->setFirstName('Bob');
$user2 = new User();
$user2->setFirstName('Alex');
$group1 = new Group();
$group1->setTitle('group1');
$group2 = new Group();
$group2->setTitle('group2');
$user1->addToGroups($group1);
$user1->addToGroups($group2);
$group2->addToUsers($user2);
$user1->save(); // Будут сохранены все группы и пользователи
$user3 = new User();
$user3->loadById($user1->getId());
echo $user3->getGroups()->at(0)->getTitle(); // Выведет 'group1'
$group3 = new Group();
$group3->loadById($group2->getId());
echo $group3->getUsers()->count(); // Выведет 2
==== Связывание объектов ====
Для добавления новых связанных объектов по одному в базовый объект (с любой стороны) используется метод **addToRelationName($object)**, в нашем случае Group :: addToUsers($user) или User :: addToGroups($group):
$user1 = new User();
$user1->setFirstName('Bob');
$user2 = new User();
$user2->setFirstName('Alex');
$group1 = new Group();
$group1->setTitle('group1');
$group2 = new Group();
$group2->setTitle('group2');
$user1->addToGroups($group1);
$user1->addToGroups($group2);
$group2->addToUsers($user2);
$user1->save(); // Будут сохранены все группы и пользователи, а также связи между ними
Можно также добавить сразу несколько объектов, используя метод **setRelationName($array_or_iterator)**, в нашем случае Group :: setUsers($users) или User :: setGroup($groups). В качестве параметра передается массив или итератор с объектами.
$user1 = new User();
$user1->setFirstName('Bob');
$user2 = new User();
$user2->setFirstName('Alex');
$group1 = new Group();
$group1->setTitle('group1');
$group1->setUsers(array($user1, $user2));
$group1->save(); // Сохранит группу и пользователей, а также связи между ними
Обратите внимание, что при использовании метода setRelationName() набор связанных объектов полностью заменяется на новый. Однако, в отличие от связи [[one_to_many|"один-ко-многим"]] связанные объекты не удаляются, удаляются только старые связи между объектами.
==== Навигация по связанных объектам ====
Метод **getRelationName()** c любой строны связи, в нашем случае это будет или User :: getGroups(), или Group :: getUsers(), возвращает итератор (коллекцию) со связанными объектами:
$user = lmbActiveRecord :: findById('User', $user_id);
$groups = $user->getGroups();
echo "User " . $user->getFirstName() . " is a member of the following groups: \n";
foreach($groups as $group)
echo $group->getTitle() . "\n";
Получив коллекцию связанных объектов можно добавлять в нее элементы посредством метода add($object), что эквивалентно вызову addToRelationName($object) у текущего объекта:
$user1 = new User();
$user1->setFirstName('Bob');
$user2 = new User();
$user2->setFirstName('Alex');
$group1 = new Group();
$group1->setTitle('group1');
$users = $group1->getUsers();
$users->add($user1);
$users->add($user2);
==== Удаление связанных объектов ====
При помощи метода **removeAll()** применительно к коллекции связанных объектов можно удалять связи между объектами (но не сами объекты), например:
$user = lmbActiveRecord :: findById('User', $user_id);
$user->getGroups()->removeAll(); // С этого момента пользователь не входит ни в одну группу
Если вам нужно удалить сами объекты, можете удалять его явно через метод lmbActiveRecord :: destroy() или lmbActiveRecord :: delete($class_name, $params = array()).
Для удаления одного объекта (связи) из коллекции используется метод **remove()**
$user = lmbActiveRecord :: findById('User', $user_id);
$user->getGroups()->remove($group_obj); // Пользователь исключён только из данной группы
Обратите внимание, что коллекции, в частности методы add() и removeAll() ведут себя по-разному, в зависимости от того, сохранен родительский объект в момент работы с коллекцией или еще нет. Подробнее об этом, а также дополнительная информация по работе с коллекциями связанных объектов можно получить в разделе [[more_on_relations|"Дополнительная информация по отношениям"]].