Итак, продолжаем изучать теорию по использованию модуля 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 месяца. потому был бы благодарен за подробную инструкцию по форку и мержу веток репозитория на примере вашего перевода документации.