Контент


Ko3: модуль Sprig. Связи

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

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в Kohana3.

Теги: , , .


Комментарии (2)

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. altspam пишет:

    Иван, спасибо за исследование модуля. Ждем финальную статью :)

  2. stelzek пишет:

    спасибо, иван, вам за статьи и за документацию на русском. очень помогло сделать быстрый старт.
    сейчас собираю каталог на sprig-mptt. было бы интересно прочитать ваш обзор об этом модуле.. возникли проблемы с «транзакциями» (блокировкой таблиц) в postgresql. пока использую самописные костыли. банально перегрузил методы.
    с репозиториями и баг-трекером знаком 2 месяца. потому был бы благодарен за подробную инструкцию по форку и мержу веток репозитория на примере вашего перевода документации.



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.