Итак, продолжаем изучать теорию по использованию модуля Sprig. Напомню, что в предыдущей статье я описал общие правила описания моделей. Но ведь сама по себе модель, отделенная от своих «соседей», не несет большого смысла и практической ценности. Поэтому необходимо рассмотреть возможности Sprig в части работы с взаимосвязями моделей.
Сообщаем о наличии связей в модели
Как мы помним, в модуле Sprig все используемые поля таблицы объявляются в методе _init()
, и поля связей тут не исключение. Есть четыре класса для описания различных типов связей (тут все похоже на связи в ORM, так что я сильно их расписывать не буду):
- Sprig_Field_HasOne
- Sprig_Field_HasMany
- Sprig_Field_BelongsTo
- Sprig_Field_ManyToMany
Названия говорят сами за себя. Поскольку в каждой связи участвует две стороны, нет смысла рассматривать классы по отдельности, так что давайте возьмем следующую ситуацию:
1. Есть записи в блоге (модель blog
).
2. Есть тэги, которые вешаются на записи блога (модель tag
).
3. Есть пользователи (модель user
).
4. Есть статистика по пользователям блога. Т.к. она не относится непосредственно к модели пользователя (мы предполагаем, что есть и другие модули, где используется user
), выделяем ее в отдельную модель (userinfo
).
В упрощенном виде схема получится примерно такая:
Тут использованы соглашения по умолчанию для ORM (первичный ключ называется `id`, внешние ключи формируются как название модели + суффикс `_id`
, и т.д.). Какие модели мы в итоге получим? Давайте я сразу приведу их код, а после разберем типы связей по отдельности.
// пользователи class Model_User extends Sprig { protected function _init() { $this->_fields += array( 'id' => new Sprig_Field_Auto, 'username' => new Sprig_Field_Char, 'email' => new Sprig_Field_Email, 'userinfo' => new Sprig_Field_HasOne, 'articles' => new Sprig_Field_HasMany, ); } } // статьи class Model_Article extends Sprig { protected function _init() { $this->_fields += array( 'id' => new Sprig_Field_Auto, 'title' => new Sprig_Field_Char, 'text' => new Sprig_Field_Text, 'user' => new Sprig_Field_BelongsTo, 'tags' => new Sprig_Field_ManyToMany, ); } } // тэги class Model_Tag extends Sprig { protected function _init() { $this->_fields += array( 'id' => new Sprig_Field_Auto, 'name' => new Sprig_Field_Char, 'articles' => new Sprig_Field_ManyToMany, ); } } // доп. информация о пользователях class Model_Userinfo extends Sprig { protected function _init() { $this->_fields += array( 'id' => new Sprig_Field_Auto, 'data' => new Sprig_Field_Text, 'user' => new Sprig_Field_BelongsTo, ); } } |
Условно все поля можно разбить на две категории — реальные поля и псевдополя. Примеры реальных полей — `id`, `email` и `username` в модели Model_User, они присутствуют в таблице. Псевдополя являются результатом наличия связей с другими моделями. Например, поля `articles` и `userinfo`, которые не могут быть сохранены в БД в явном виде. Для них свойство
$in_db
установлено в FALSE. Не стоит путать внешний ключ (например, `user_id` в таблице `articles`) с псевдополем `user` — в первом случае мы просто работаем с численным значением, второй вариант дает возможность получить связанный объект.
HasOne
Примером данной связи будет наличие доп. информации у пользователя. В модели Model_User объявлено псевдополе $userinfo
:
'userinfo' => new Sprig_Field_HasOne, |
Так как никаких дополнительных параметров при создании псевдополя не передавалось, то при обращении к нему будут автоматически вычислены следующие свойства связи:
- Имя связанной модели `model` будет `userinfo` (по названию псевдополя).
- Имя внешнего ключа в той модели (`column`) определится как `user_id` (
имя текущей модели + '_id'
).
Схема определения имени внешнего ключа не является жестко фиксированной — используется метод
fk($table = NULL)
модели. Вы можете его переопределить, чтобы, к примеру, возвращать ‘user_key‘. Кроме того, если в Вашей модели используется нестандартный первичный ключ (например ‘key‘), то по умолчанию методpk()
будет возвращать что-то вроде ‘user_key‘ вместо ‘user_id‘ — будьте внимательны!
Теоретически, теперь мы можем попробовать обратиться к свойству $user->userinfo
:
$user = Sprig::factory('user', array('id' => 1)); echo $user->userinfo->load()->data; |
К сожалению, результата не будет. Что-то не так? Методом проб и ошибок определяем, что метод load() не возвращает ожидаемое (а мы предполагаем модель Userinfo, где поле ‘user_id‘ равно 1). А проблема вот в чем — надо объявить данную связь на другой ее стороне, т.е. собственно в модели Userinfo. Это связь типа BelongsTo, так что имеет смысл перейти к ней.
BelongsTo
Через данный тип связи объявляем, что каждый объект Userinfo привязан к родительскому объекту User, в нашем случае через поле ‘user_id‘. Так как пока что все свойства рассматриваемых моделей (ключи, имена связей и т.д.) максимально простые, то и объявление будет коротким:
'user' => new Sprig_Field_BelongsTo, |
Автоматически заполняются свойства связи:
- ‘model‘ — имя связанной модели, в данном случае ‘user‘.
- ‘column‘ — имя поля внешнего ключа, получаем через метод
fk()
модели Model_User (по умолчанию вернет ‘user_id‘).
Поскольку данный тип связи используется не только для HasOne, но и с HasMany, аналогичное объявление мы сделали в модели Model_Article для связи с Model_User:
'user' => new Sprig_Field_BelongsTo, |
Теперь можно вызывать $userinfo->user->load()->username
, причем обратную сторону связи в данном случае прописывать необязательно. Таким образом, делаем вполне логичный вывод — связь обязательно должна быть прописана в пассивной модели (т.е. в модели, в которую переходит внешний ключ).
HasMany
Собственно тут тоже ничего сложного, вот так в Model_User объявлено, что каждый пользователь может понаписать множество статей в блоге:
'articles' => new Sprig_Field_HasMany, |
«На автопилоте» определяется свойство ‘model‘ — имя модели, получаем как название связи в единственном числе (через Inflector::singular()
) — ‘article‘. Свойство ‘column‘ бессмысленно, т.к. в результате данной связи в таблице никаких полей не добавляется.
ManyToMany
Тут тоже все просто и скучно, в модели Model_Article объявляем о тэгах:
'tags' => new Sprig_Field_ManyToMany, |
- Свойство ‘model‘ получится как ‘tag‘ (имя связи в единственном числе).
- Свойство ‘through‘ (имя промежуточной таблицы) вычисляется аналогично ORM: берем имена таблиц (в данном случае это ‘articles‘ и ‘tags‘), ставим по алфавиту и разделяем знаком подчеркивания, т.е. ‘articles_tags‘ в итоге.
- Свойство ‘column‘ не заполняется ввиду неактуальности.
Что любопытно — после вычисления свойства ‘through‘ для пары статья-тэг становится доступным статическое свойство
Sprig::$_relations['article_tag']
, которое содержит имя промежуточной таблицы. Таким образом, если из модели Model_Tag обратиться к свойствуarticles
, промежуточная таблица будет взята оттуда. Теоретически это должно позволить менять одну из главных настроек связи ManyToMany только в одном месте, но у меня это не вызвало особого интереса. Наверное проще указать ‘through‘ вручную в двух моделях, зато это будет предсказуемо.
Ленивая загрузка
Наверняка вы обратили внимание на метод load()
, который иногда используется после обращения к связанным объектам. Очевидно, что данный метод вызывает загрузку записей, т.е. непосредственно $user->userinfo
, к примеру, вернет только экземпляр Database_Query_Builder_Select с примененными параметрами выборки (с условием where('user_id', '=', 1)
). В общем, lazy loading к вашим услугам! Правда, это актуально только для HasOne и BelongsTo.
Идем дальше. Наверняка вы помните, как осуществляется выборка связанных объектов в ORM. К примеру, $user->articles->where('approved', '=', 1)->find_all()
позволяет выбрать только утвержденные статьи пользователя. Однако в Sprig не все так просто. К сожалению, непосредственный вызов $user->articles
приведет к выборке всех статей пользователя, метод where()
здесь просто некуда «воткнуть». Единственное, что предлагается нам — все тот же метод load()
. В результате то, что в ORM получается достаточно элегантно, в Sprig выглядит несколько неуклюже:
$query = DB::select()->where('user_id', '=', $user->pk())->where('approved', '=', 1); $articles = Sprig::factory('article')->load($query, FALSE); |
Сперва подготовили условия (авторство и наличие флага «утверждено»), потом подставили в метод load()
. Не забудьте про второй параметр, т.к. по умолчанию количество записей ограничивается одной. Впрочем, можно установить нужный лимит записей, например для разбивки на страницы (т.е. для пагинатора).
Это актуально и для безусловных выборок. Например, получаем всех пользователей:
$users = Sprig::factory('user')->load(NULL, FALSE);
Да, это работает. Но теряется вся красота и прозрачность, которая сопутствует ORM. Надеюсь, это будет исправлено в следующих коммитах.
Усложняем задачу
Конечно, далеко не всегда получается использовать именования таблиц, полей и т.д., которые по умолчанию подходят под соглашения, будь то ORM или Sprig. Поэтому давайте изменим рассматриваемую схему, которая во второй редакции будет выглядеть вот так:
Список изменений следующий.
- Таблица articles_tags переименована в arttags.
- Первичный ключ в articles теперь называется articleid.
- Внешний ключ user_id переименован в author_id.
Теперь посмотрим, как изменятся коды моделей. Для начала разберемся с переименованием первичного ключа. Обычно (и в ORM мы используем такой подход) для этого есть специальное свойство $_primary_key
. Но одна из особенностей Sprig — некоторые типы полей могут быть заранее обозначены как часть первичного ключа (их свойство $primary
установлено в TRUE). Это действительно для поля Sprig_Field_Auto, поэтому поле `articleid` будет добавлено в свойство $_primary_key
автоматически. Другое дело, что в методе _init()
, где это поле объявляется, необходимо указать актуальное имя:
protected function _init() { $this->_fields += array( 'articleid' => new Sprig_Field_Auto, ... |
Стоит обратить внимание, что текущая версия Sprig не осуществляет проверку при добавлении поля в список первичного ключа, так что в случае прописывания свойства
$_primary_key
вручную скорее всего одно и то же поле будет добавлено туда дважды.
Идем дальше. Для связи с пользователем модель Article теперь использует внешний ключ `author_id`. В связи с этим меняем объявление поля в методе _init()
:
'user' => new Sprig_Field_BelongsTo(array('column' => 'author_id'), |
Как мы видим, чтобы переопределить дефолтные названия для внешних ключей, надо использовать параметр column
. Сделаем еще одно изменение — пусть доступ к модели User будет осуществляться по псевдополю ‘author‘ (т.е. $article->author
вместо $article->user
, что более логично). Для этого надо переименовать имя поля, а также явно указать название связанной модели, которое ранее вычислялось автоматически:
'author' => new Sprig_Field_BelongsTo(array('model' => 'user', 'column' => 'author_id'), |
Ну и напоследок — указываем в явном виде новое имя промежуточной модели для связи ManyToMany:
'tags'=> new Sprig_Field_ManyToMany(array('through' => 'arttags')), |
Прописали? Проверяем, работает?! Ан нет, не работает. Вспомните алгоритм вычисления наименования внешнего ключа — по умолчанию все зависит от первичого ключа. Поэтому JOIN таблиц arttags и tags будет осуществляться по некорректному полю ‘article_articleid‘. Как этого избежать, я уже упоминал — надо переопределить метод fk()
, чем нам и придется заняться:
public function fk($table = NULL) { $key = 'article_id'; if ($table) { if ($table === TRUE) { $table = $this->_table; } return $table.'.'.$key; } return $key; } |
Конечно, не самое красивое решение. Во первых, приходится копировать исходный метод полностью, а хотелось бы в полной мере попользоваться наследованием, дабы в своем методе (условно говоря) вычислить переменную $key
(или сохранить ее в каком-нибудь свойстве с говорящим названием $_foreign_key
) и далее вызвать parent::fk()
. Кроме того, вдруг в какой-нибудь другой таблице внешний ключ будет переходить под другим названием? Единственное приходящее мне в данный момент решение — добавление switch ... case
с перечислением известных вариантов значений для переменной $table
(по крайней мере для ManyToMany метод fk()
всегда использует данный параметр). Вспоминается поговорка про мышей и кактус
На этом все
Согласитесь, налицо существенные недостатки и никаких преимуществ перед ORM. Так почему я пишу о данном модуле, а не придумываю новый повод написать очередную статью про ORM? Намекну, что главная сила Sprig не в связях. Sprig удобен тем, что мы унифицируем работу с полями модели/таблицы — генерацию и обработку форм в первую очередь. Но это уже тема для следующей — итоговой — статьи, в которой я покажу, что нам собственно делать с предоставляемыми возможностями.
Иван, спасибо за исследование модуля. Ждем финальную статью
спасибо, иван, вам за статьи и за документацию на русском. очень помогло сделать быстрый старт.
сейчас собираю каталог на sprig-mptt. было бы интересно прочитать ваш обзор об этом модуле.. возникли проблемы с «транзакциями» (блокировкой таблиц) в postgresql. пока использую самописные костыли. банально перегрузил методы.
с репозиториями и баг-трекером знаком 2 месяца. потому был бы благодарен за подробную инструкцию по форку и мержу веток репозитория на примере вашего перевода документации.