====== Дополнительная информация по отношениям ======
===== Поддержка отношений в методе lmbActiveRecord :: import() =====
Метод lmbActiveRecord :: import() используется для заполнения/обновления объектов. Если метод import() встречает в списке переданных ему полей имя, сответствующее названию одного из отношений, он пытается заполнить полученные данные в соответствие с описанием отношения.
==== Для отношений один-к-одному ====
Возьмем пример, описанный в разделе [[one_to_one|"Поддержка отношений вида один-к-одному"]]:
class Person extends lmbActiveRecord
{
protected $_has_one = array('social_security' => array('field' => 'social_security_id',
'class' => 'SocialSecurity',
'can_be_null' => true));
}
class SocialSecurity extends lmbActiveRecord
{
protected $_belongs_to = array('person' => array('field' => 'social_security_id',
'class' => 'Person'));
}
Используем import для того, чтобы связать %%SocialSecurity%% с Person.
$person = new Person();
$person->setName('Jim');
$person->save();
[[...]]
$data = array('code' => '099123', 'person' => $person->getId());
$number = new SocialSecurity();
$number->import($data);
$number->save();
Обратите внимание, что поле называется именно person - по названию связи, а не person_id.
Где может использоваться такая возможность? Обычно она используется в панелях управления, где можно передавать идентификатор связанных объектов прямо в запросе и передавать данные в объекты посредством вызовов вида:
$number = new SocialSecurity();
$number->import($request->export());
$number->save();
Отметим, что это такой способ заполнения объекта данными подходит не всегда: например, если значение поля, соответствующее связи, в пришедшем в import() массиве не указывает на реальный объект. В этом случае может произойти исключение, так как при импорте lmbActiveRecord постарается загрузить указанный объект через findById() метод (см. [[crud|"Создание, сохранение, загрузка и удаление объектов"]]).
==== Для отношений один-ко-многим и много-ко-многим ====
Если в import в соответствующем поле пришел массив идентификаторов, эти объекты будут сохранены, как если бы мы вызвали метод setRelationName($objects).
Возьмем пример, описанный в разделе [[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'));
}
Используем import для того, чтобы связать User с Group.
$group1 = new Group();
$group1->setTitle('First group');
$group1->save();
$group2 = new Group();
$group2->setTitle('Second group');
$group2->save();
[[...]]
$data = array('first_name' => 'Vasa', 'groups' => array($group1->getId(), $group2->getId()));
$user = new User();
$user->import($data);
$user->save(); // Теперь пользователь Vasa входит в 2 группы.
// или
$data = array('first_name' => 'Vasa', 'groups' => array($group1, $group2));
$user = new User();
$user->import($data);
$user->save(); // Теперь пользователь Vasa входит в 2 группы.
Аналогично для отношений один-ко-многим
===== Работа с коллекциями связанных объектов =====
Рассмотрим отношение один-ко-многим, которое мы приводили чуть выше:
class Course extends lmbActiveRecord
{
protected $_has_many = array('lectures' => array('field' => 'course_id',
'class' => 'Lecture'));
}
class Lecture extends lmbActiveRecord
{
protected $_belongs_to = array('course' => array('field' => 'course_id',
'class' => 'Course'));
}
Наша задача - рассмотреть подробнее коллекции связанных объектов, то, какими методами можно пользоваться, чтобы изменять состояние этих коллекций.
==== Изменение содержимого коллекции ====
Итак, коллекцию, которую возвращает метод getLectures() можно изменять, например, добавлять в нее новые элементы:
$course = new Course();
$course->setTitle('Super course');
$l1 = new Lecture();
$l1->setTitle('Physics');
$lectures = $course->getLectures();
$lectures->add($l1); // Эквивалентно $course->addToLectures($l1);
Также из коллекции можно удалить все объекты, используя метод removeAll().
==== Доступ к элементам внутри коллекции ====
Коллекция связанных объектов - это обычный итератор:
$lectures = $course->getLectures();
for($lectures->rewind(); $lectures->valid(); $lectures->next())
{
$lecture = $lectures->current();
echo $lecture->getTitle();
}
// Возможна более компактная запись
foreach($course->getLectures() as $lecture)
echo $lecture->getTitle();
Можно получать доступ к определенным объектам по индексу при помощи метода **at($pos)**, это полезно при тестировании:
$lectures = $course->getLectures();
echo $lectures->at(2)->getTitle();
Обратите внимание, что каждый вызов at() приводит к отдельному запросу к базе данных.
Также можно получить массив объектов при помощи метода **getArray()** и обращаться к объектами по порядковому номеру:
$lectures = $course->getLectures()->getArray();
echo $lectures[1]->getTitle();
Также можно получить только список идентификаторов связанных объектов при помощи метода getIds():
$ids = $course->getLectures()->getIds();
echo implode(',', $ids); // Выведет что-то вроде 2,10,11,20...
==== Поиск элементов в коллекциях ====
Для поиска элементов внутри коллекция существуют find-методы, подобные тем, которые реализованы для класса lmbActiveRecord.
Метод **find($params = array())** - осуществляет поиск элементов внутри коллекции. Аргумент $params действует аналогично тому, что используется в lmbActiveRecord :: find(). То есть вместо $params, можно передать строку или объект [[limb3:ru:packages:dbal:criteria|Criteria]], а может передать массив с некоторыми полями, которые будут содержать информацию по сортировке, например:
$course = new Course();
$short_lectures = $course->getLectures()->find('duration < 30');
$criteria = new lmbSQLFieldCriteria('duration ', 60, '>');
$sort = array('title' => 'DESC', 'duration' => 'ASC');
$course->getLectures()->find(array('criteria' => $criteria, 'sort' => $sort));
Также существует метод **findFirst($params = array())**, который возвращает первый найденный элемент из коллекции.
==== Сортировка ====
Если find-методы ничего не получают про то, как нужно сортировать элементы при итерации, тогда они используют параметры сортировки по-умолчанию.
Параметры сортировки по-умолчанию также применяются при обычном итерировании по элементам коллекции.
Параметры сортировки по-умолчанию формируются следующим образом:
* они могут быть указаны в описании отношений,
* они берутся из свойств класса, объекты которого хранятся в коллекции.
В первом случае сортировка может быть указана в виде параметра sort_params при описании отношений, например:
class Course extends lmbActiveRecord
{
protected $_has_many = array('lectures' => array('field' => 'course_id',
'class' => 'Lecture',
'sort_params' => array('title' => 'ASC')));
}
Обратите внимание на **sort_params**. То есть теперь элементы коллекции lectures будут сортироваться по заголовку, если не будет указано иное:
$lecture1 = new Lecture(array('title' => 'Super lecture'));
$lecture2 = new Lecture(array('title' => 'Basic lecture'));
$course = new Course(array('title' => 'My lecture'));
$course->setLectures(array($lecture1, $lecture2));
$course->save();
$lectures = $course->getLectures()->find();
$lectures->rewind();
echo $lectures->current()->getTitle(); // Выведет Basic lecture
$lectures->next();
echo $lectures->current()->getTitle(); // Выведет Super lecture
Если же параметр sort не будет указан, тогда элементы коллекции (пусть будет lectures), будут сортироваться так, как это определено для класса Lecture (см. [[limb3:ru:packages:active_record:find#sortirovka_obektov_pri_zagruzke|сортировку в классе lmbActiveRecord]]).
==== Различия в поведении коллекций для новых и уже сохраненных объектов ====
:!: Обратите внимание, что коллекции отношений по разному себя ведут если родительский объект (владелец коллекции) является новым, то есть еще несохраненным в базе, и если он уже был сохранен.
Для коллекции уже сохраненного владельца вызов метода add(), find(), findFirst(), removeAll() приводят к обращению к базе данных. После вызова add() новый объект сразу же сохраняется (в случае с много-ко-многим еще добавляется запись в связывающую таблицу).
Длу коллекции несохраненного владельца метод add() и removeAll() не приводят к обращению к базе данных. Они лишь изменяют состояние набора объектов в памяти. То есть в "новой" коллекции объекты "копятся", и сохраняются только при сохранении владельца. Обратите внимание, что метод find() и findFirst() для "новых" коллекций не работают и генерируют исключение.
===== Изменение объектов в коллекциях =====
Если вы получили коллекцию связи один-ко-многим или много-ко-многим через родительский объект (или просто объект с одной из строн), и изменяете их, итерируя по коллекции, вы должны сохранять объекты явно. Сохранение только родительского объекта в текущей версии не достаточно:
$course = new Course($course_id);
foreach($course->getLectures() as $lecture)
$lecture->setTitle($lecture->getTitle() . ' appended');
$course->save(); // Не приведет к сохранению изменений в lectures.
Необходимо писать:
$course = new Course($course_id);
foreach($course->getLectures() as $lecture)
{
$lecture->setTitle($lecture->getTitle() . ' appended');
$lecture->save();
}
В будущих версиях это поведение планируется поправить, однако сейчас - будьте внимательны!
===== Установка полей отношений напрямую. =====
lmbActiveRecord для отношений **has_one** и **many_belongs_to**, то есть для тех, которые предусматривают наличие поля связи в таблице класса, работу с этими полями напрямую.
То есть, можно взять и поставить значение соответствующего поля явно, например:
$lecture->set('course_id', $course_id);
В этом случае в таблицу lecture в поле course_id запишется $course_id.
Однако, если вы в это же время, попробуете работать со связанным объектом, тогда ранее поставленное поле напрямую уже учитываться не будет, то есть:
$lecture->set('course_id', $course_id);
$lecture->setCourse($my_course); // будет использован $my_course->getId() для поля course_id вместо $course_id
Напомним, что если вместо:
$lecture->set('course_id', $course_id);
вызвать:
$lecture->set('course', $course_id);
то второй пример приведет к загрузке объекта Course с идентификатором $course_id.
===== Обнуление связей =====
lmbActiveRecord для отношений **has_one** и **many_belongs_to** также предусмотрено обнуление отношения. Для этого нужно или в качестве объекта связи указать null, или в значение поля таблицы указать null, например:
$lecture->setCourse(null);
// или
$lecture->set('course_id', null);
Убедитесь, что описание отношения имеет флаг **can_be_null**
===== Дополнительные возможности при описании отношений =====
При наличии сложных иерархий в рамках %%ActiveRecord%% возникает ситуация, когда в дочерних классах появляется необходимость расширить описания отношений, по сравнению с родительским классом. Для этих целей есть метод **_defineRelations()**, который вызывается в конструкторе lmbActiveRecord.
Например:
class BaseNews extends lmbActiveRecord
{
protected $_db_table_name = 'news';
protected $_has_many = array('publications_in_sections' => array('field' => 'news_id',
'class' => 'SectionNewsPublish'),
'related_links' => array('field' => 'news_id',
'class' => 'NewsRelatedLinks'),
'related_news' => array('field' => 'news_id',
'class' => 'rtNewsRelatedNews'));
}
class VersionedNews extends BaseNews
{
protected function _defineRelations()
{
$this->_has_many['version'] = array('field' => 'original_id',
'class' => 'NewsVersion',
'sort_params' => array('version' => 'DESC'));
}
}
Расширять описания отношений можно как напрямую, добавляя элементы в соответствующие атрибуты класса, так и при помощи методов, например, _hasOne($relation_name, $relation), _hasMany($relation_name, $relation) и т.д.:
class VersionedNews extends BaseNews
{
protected function _defineRelations()
{
$this->_has_many('version', array('field' => 'original_id',
'class' => 'NewsVersion',
'sort_params' => array('version' => 'DESC'));
}
}
Какой способ предпочесть - зависит от вашего вкуса.
==== Динамическое описание отношений ====
При необходимости, можно динамически указывать отношения. Например, такой код в зависимости от наличия в таблице поля ''lmb_cms_user_id'' задаст соответствующее отношение
function _defineRelations()
{
if ($this->has('lmb_cms_user_id'))
{
$this->_hasOne('owner', array('field' => 'lmb_cms_user_id',
'class' => 'lmbCmsUser'));
}
}
===== Условия в отношениях =====
Иногда возникает необходимость иметь в наследнике %%ActiveRecord%% метод, который не просто получает коллекцию связанных объектов, но и накладывает на нее определенные условия выборки. В этом случае нужно задать параметр criteria в описании отношений.
protected $_has_many_to_many = array('groups' => array('field' => 'user_id',
'foreign_field' => 'group_id',
'table' => 'user2group',
'class' => 'Group'),
'active_groups' => array('field' => 'user_id',
'foreign_field' => 'group_id',
'table' => 'user2group',
'class' => 'Group',
'criteria' =>'`group`.`is_active`=1'
));
После создания такого описания отношений мы будем иметь два метода: getGroups для получения всех связанных с пользователем групп и getActiveGroups - для получения связанных с пользователем групп, у которых признак активности равен единице.
$groups = $user->getGroups();
// или
$groups = $user->getActiveGroups();
Возможность накладывать criteria на коллекции связанных объектов поддерживается как для _has_many_to_many , так и для _has_many .
===== Использование 1-й таблицы для нескольких отношений =====
lmbActiveRecord позволяет иметь несколько отношений many_to_many (и другие, если необходимо) в одной таблице. Например, у нас был случай, когда таблица, которая хранила связи, была следующей:
CREATE TABLE `related_forum_topic` (
`id` bigint(20) NOT NULL auto_increment,
`topic_id` bigint(20) default NULL,
`is_main` tinyint(4) default NULL,
`section_id` bigint(20) default NULL,
`news_id` bigint(20) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
А связи описывались следующим образом:
class News extends lmbActiveRecord
{
protected $_has_many_to_many = array('related_forum_topics' => array('field' => 'news_id',
'foreign_field' => 'topic_id',
'class' => 'ForumTopic',
'table' => 'related_forum_topic'));
}
class Section extends lmbActiveRecord
{
protected $_has_many_to_many = array('related_forum_topics' => array('field' => 'section_id',
'foreign_field' => 'topic_id',
'class' => 'ForumTopic',
'table' => 'related_forum_topic'));
}
class ForumTopic extends lmbActiveRecord
{
protected $_has_many = array('link_to_me' => array('field' => 'topic_id',
'class' => 'RelatedForumTopic'));
protected $_has_many_to_many = array('sections_links' => array('field' => 'topic_id',
'foreign_field' => 'section_id',
'class' => 'Section',
'table' => 'related_forum_topic'),
'news_links' => array('field' => 'topic_id',
'foreign_field' => 'news_id',
'class' => 'News',
'table' => 'related_forum_topic'));
}
class RelatedForumTopic extends lmbActiveRecord
{
protected $_many_belongs_to = array('topic' => array('field' => 'topic_id',
'class' => 'ForumTopic'));
}
Таким образом, мы хранили 2 отношения many-to-many и 1 one-to-many в 1 таблице related_forum_topic. При обновлении записей отношений 'sections_links' и 'news_links' класса %%ForumTopic%% из таблицы related_forum_topic удалялись только те записи, где значение foreign_field не было равно null.