Возвращаюсь к одной из самых популярных тем, связанных с фреймворком Kohana — библиотека ORM. В новом релизе Ko3 изменился и ORM, причем кардинально. Месяц назад я уже упоминал об отдельных изменениях, теперь же можно рассчитывать на стабильность данного модуля и начать описывать изменения по сравнению с веткой 2.3.4.
Организационные изменения
- Ну, что теперь ORM поставляется в виде отдельного модуля, который надо подключать через bootstrap.php, думаю, вы знаете. На всякий случай напомню, что для работы с БД также необходимо подключать модуль Database.
- Из модуля выброшены классы ORM_Tree и ORM_Versioned, хотя навскидку там потребуется немного работы для портирования…
- Основной класс (Kohana_ORM) находится в classes/kohana/orm.php, для возможного расширения/модификации модуля предоставлен пустой класс ORM в classes/orm.php.
Создание модели
- Метод
unique_value()
, который ранее позволял вместо первичного ключа использовать другие уникальные поля таблицы (типа email или name), теперь исключен из библиотеки. Соответственно вместоORM::factory('role', 'admin')
придется писатьORM::factory('role', array('name'=>'admin'))
. И вообще, при передаче в конструктор массива значений они будут перечислены в конструкциюwhere
для дальнейшего поиска. - Если первичный ключ в конструктор не передан, будет произведена попытка загрузить данные из свойства
$_preload_data
. Таким образом, можно создавать модель с какими-либо начальными данными без обращения к БД.
Свойства и методы модели
- Все защищенные (
protected
) свойства моделей теперь начинаются со знака подчеркивания (например$_table_name
). Ах да, в новом ORM нетpublic
свойств - Привычных свойств типа
loaded
илиsaved
нет (точнее они теперь относятся к защищенным свойствам). Доступ к ним предоставляется через вызов метода с аналогичным названием, например$user->loaded()
или$user->table_name()
. Перечень допустимых методов можно увидеть в свойстве$_properties
(в частности, секции, прокомментированные как Object и Table):
// Members that have access methods protected static $_properties = array ( 'object_name', 'object_plural', 'loaded', 'saved', // Object 'primary_key', 'primary_val', 'table_name', 'table_columns', // Table 'has_one', 'belongs_to', 'has_many', 'has_many_through', 'load_with', // Relationships 'validate', 'rules', 'callbacks', 'filters', 'labels' // Validation );
Все свойства доступны только для чтения (за исключением разве что объекта
$_validate
). - Метод
as_array()
теперь возвращает не только значения текущей таблицы, но и уже загруженные значения объектов, связанных через$_has_one
с текущим.
Связи
Тут произошла целая революция.
- Изменилась структура свойств
$_has_one
,$_has_many
и$_belongs_to
. В общем виде связь состоит из названия связи (псевдоним или alias), имени связанной модели и имени поля внешнего ключа в этой связи. Приведу пример на основе баянистых взаимоотношений в блоге:// Model_Blog protected $_has_many = array( 'comments' => array( 'model' => 'comment', 'foreign_key' => 'blog_id', ), ); // Model_Comment protected $_belongs_to = array( 'blog' => array( 'model' => 'blog', 'foreign_key' => 'blog_id', ), );
Для модели «блог» указываем наличие комментариев (имя модели — ‘comment‘, имя внешнего ключа для нее — ‘blog_id‘). Для модели «комментарий» объявляем принадлежность к модели ‘blog‘ и имя используемого внешнего ключа — ‘blog_id‘. Обратите внимание, что имя внешнего ключа берется от слабой модели, т.е. в обоих случаях это модель Model_Comment. Для связи один-к-одному все в принципе то же самое (внешний ключ выносится в зависимую модель, как в$_has_many
). - Связь много-ко-многим (has_and_belongs_to_many или HABTM) вполне логично заменена использованием двух связей один-ко-многим (has_many). В связи с появлением дополнительной промежуточной таблицы описание связи несколько усложнилось. Давайте рассмотрим связь между пользователем (user) и ролью (role) со стороны пользователя:
// Model_User protected $_has_many = array( 'roles' => array( 'model' => 'role', 'foreign_key' => 'user_id', 'through' => 'roles_users', 'far_key' => 'role_id', ), );
Добавились параметры through и far_key. Первый отвечает за имя промежуточной таблицы, второй — за имя внешнего ключа в ней для связанной модели. Т.е. в данном примере используется промежуточная таблица role_users с внешними ключами user_id (id пользователя) и role_id (id роли). Если параметр through не указан (или установлен в NULL), то данная связь будет трактоваться как «чистая» has_many, а иначе — как HABTM.
Конечно же, в вышеперечисленных примерах все параметры являются достаточно предсказуемыми, и было бы нелогично не реализовать подстановку значений по умолчанию. Так, для параметра model по умолчанию будет поставлено значение псевдонима связи (alias) в единственном числе, foreign_key вычисляется как имя сильной модели (для
$_has_one
и$_has_many
это$this->_object_name
, а для$_belongs_to
— значение alias) + суффикс (свойство$_foreign_key_suffix
, по умолчанию ‘_id‘). Параметр through по умолчанию установлен в NULL, а far_key вычисляется как псевдоним связи (в ед. числе) + все тот же суффикс. Если интересно посмотреть на эти вычисления, загляните в метод_initialize()
. - Методы для работы с HABTM (
has()
,add()
иremove()
) тоже немного поменялись. Теперь в качестве первого параметра выступает имя связи, второй параметр — связанная ORM-модель для поиска/добавления/удаления связи соответственно. Например,$user->add('roles', ORM::factory('role', 1));
добавит пользователю$user
роль с идентификатором 1. Кроме того, методыadd()
иremove()
выполняются сразу (напомню, что ранее надо было сохранять текущую модель).
Общая работа с моделью
- Отдельно надо сказать о ленивой загрузке. При создании модели, даже с передачей параметра
$id
в конструктор, немедленного запроса к БД не будет. Это очень удобно, когда надо передать в куда-то ORM-модель, все необходимые поля которой известны. Например, при работе с методами HABTM. Запрос к БД будет выполнен при попытке получить незагруженные данные или обращению к какому-либо специфическому методу (например, кloaded()
). Стоит особо обратить внимание на то, что объекты с несколькими записями придется явно «пинать» с помощью методаfind_all()
. Например:$blog = ORM::factory('blog', 2); // объект создался без обращения к БД // следующая запись работать не будет: foreach ($blog->comments as $comment) {...} // $blog->comments пока что объект класса Model_Comment // а вот как правильно: foreach ($blog->comments->find_all as $comment) {...}
Вообще придется привыкать к тому, что однозначные связи ($_has_one и $_belongs_to) будут загружены сразу, а вот при работе с $_has_many есть нюанс. Зато доступны дополнительные фильтры, например так:
$blog->comments->where('moderated', '=', 1)->limit(10)->find_all()
. - Теперь можно изменять значения полей, упомянутых в
$_ignored_columns
. Кроме того, можно напрямую изменять связанную через$_belongs_to
модель (точнее привязать текущий объект к другой модели). Например,$comment->blog = ORM::factory('blog', 2)
. Только после этого надо не забыть сохранить модель$comment
. - Метод
load_values(array $values)
переименован вvalues(array $values)
. Кроме собственно значений текущей модели можно изменять значения однозначно связанных моделей (т.е. через связи$_has_one
и$_belongs_to
). Для этого имя поля должно иметь вид ‘alias:column‘, где alias — это имя используемой связи.
Работа с БД
- Для работы с базами данных помимо объекта
$_db
(объект класса Database>) теперь добавлен объект$_db_builder
(потомок класса Database_Query_Builder, в зависимости от типа используемого запроса). Это связано с разделением класса Database на отдельные составляющие, о чем я писал ранее. Методы QBuilder‘а, как и раньше, можно применять напрямую, они описаны в свойстве$_db_methods
:// Callable database methods protected static $_db_methods = array ( 'where', 'and_where', 'or_where', 'where_open', 'and_where_open', 'or_where_open', 'where_close', 'and_where_close', 'or_where_close', 'distinct', 'select', 'from', 'join', 'on', 'group_by', 'having', 'and_having', 'or_having', 'having_open', 'and_having_open', 'or_having_open', 'having_close', 'and_having_close', 'or_having_close', 'order_by', 'limit', 'offset', 'cached' );
Однако на данные методы также будет применена пресловутая «ленивая» загрузка. Все вызовы QBuilder‘а будут аккуратно и по порядку сложены в свойстве$_db_pending
, чтобы потом последовательно сформировать запрос. Собственно запрос инициируется внутренним методом_build()
, который может быть вызван изfind()
,find_all()
,save_all()
,delete_all()
илиcount_all()
. - Метод
find_all()
освобожден от параметров (в 2.3.4 там были$limit
и$offset
), теперь доступны отдельные методыlimit()
иoffset()
. - Метод
list_fields($table)
переименован вlist_columns()
и возвращает список полей только для текущей модели.
Валидация
Тут тоже много изменений. Если раньше метод validate()
на входе принимал объект Validation, то теперь все происходит внутри модели.
- Сам объект представлен свойством
$_validate
, а настроить правила/фильтры/callback‘и можно через свойства$_rules
,$_filters
,$_callbacks
соответственно. Вот пример добавления пары правил для модели Model_User:
protected $_rules = array ( 'username' => array ( 'not_empty' => NULL, 'min_length' => array(4), 'max_length' => array(32), ), 'password' => array ( 'not_empty' => NULL, 'min_length' => array(5), 'max_length' => array(42), ), );
Правила сгруппированы по имени поля, внутри используется конструкция в виде пар `правило` => `параметры`. С фильтрами и обратными вызовами то же самое. Кроме того, есть массив$_labels
с «человечными» псевдонимами для полей (для вывода ошибок валидации). Извне просмотреть их можно получить через вызов аналогичного метода (смотрим секцию Validate свойства$_properties
). - Вместо метода
validate()
используетсяcheck()
без параметров. Это означает, что валидацию всегда будет проходить именно текущие значения объекта. Чтобы проверить значение массива$_POST
, придется сперва его загрузить в модель с помощью методаvalues()
. - Как такового метода
errors()
в ORM нет, придется сперва запросить объект Validate через методvalidate()
, а уже у него запрашивать ошибки:foreach($user->validate()->errors()) {...}
.
Навскидку вроде все. Остались небольшие недоработки и шероховатости (к примеру, непонятно зачем присутствует свойство $_foreign_key
), но это все дело времени. В целом все совсем даже неплохо.
Только решил перелезть с Ci на Ко и начал читать доку на их сайте, а тут они выпускают Ко3 с такими изменениями, что та документация становится неактуальной…
так что пишите чаще и больше! (-: лично я буду благодарен (-:
К сожалению, не всегда удается выделить время и подобрать удачную тему… Но я над этим работаю. Спасибо, что читаете.
По поводу документации:
1. Многие секции документации (файловая система, отдельные хэлперы и библиотеки) остаются актуальными и для Ko3.
2. Не забываем, что 2.3.4 никуда не делась, да и 2.4 будет все-таки ближе к ветке 2.3, чем к 3.0.
PS. А с CI слезайте, у нас веселее
BIakaVeron, большое спасибо за статью. Только что «рыл» официальный форум, многих ответов так и не нашел. А тут все предельно понятно, разложено по полочкам.
Ждем мануал про роуты, а то в статье на Хабре совсем не понятно, как же сделать какой-нибудь параметр необязательным.
CI подкупает своей документацией и простотой (-:
Да ну понятно, что 2.х никуда не делась, но рано или поздно ветка закроется, а портировать на Ко3 будет не самая весёлая процедура…
А как можно с помошью ORM выбрать всех пользователей у которых есть хотя бы одна стотья в блогах?
Самый простой вариант (не в плане формирования SQL-запроса, а в смысле логики) — это собрать пользователей в два шага:
1. Получаем все id пользователей. Используем для этого Query Builder, так более практично.
Чтобы не тащить все записи из таблицы, делаем выборку только поля `user_id`. Но чтобы еще больше сэкономить, добавим слово DISTINCT к запросу, так мы получим идентификаторы без повторов.
2. Получаем пользователей на основании этих id.
Но более практичный (как мне кажется) вариант — это создание доп. таблицы типа user_stats, связанной с users через has_one — belongs_to. В нее можно складывать различные счетчики (количество посещений блога, количество статей и комментариев) и т.д. В таком случае пользователей можно будет получить следующим образом:
Работоспособность последнего моего запроса подтвердить на все 100% не могу, но должно быть примерно так.
@BlakaVeron: Great article! Even though I don’t know Russian ( Only Hebrew and English ), I used the translate.google.com to read it. Question: Do you know if the ORM method ->values() cleans variables from XSS attacks?
Idea for your next article: KO3 and Security
@Rafi B.
No,
values()
just set column values, without any security checks. But you can apply Security::xss_clean() as validation filter for fields you need clear (for example, xss_clean may be skipped for integer fields ).Thanks for response.
Появился еще один вопрос про связи между таблицами и оптимизацию запросов. Для тем форума нужно получить дату и краткий текст последнего поста, ну и его автора. В таблице
topics
завел полеlast_post_id
, где хранится id последнего поста. Хотел использовать функциюwith()
для получения нужной мне записи, но связывание таблиц происходит по полямtopic.id - post.topic_id
, а неtopic.last_post_id - post.id
, хотя в моделях явно указалforegin_key
и, на всякий случай,far_key
:Topic_Model:
Post_Model:
Наверно самым простым решением было бы создать дополнительную таблицу, где хранить информацию по топикам, но уж очень не хочется плодить лишние сущности.
Тут необходимо учитывать, что если мы храним идентификатор поста в топике, то получается отношение belongs_to (если смотреть со стороны топика). Так что:
Большое спасибо за наведение на правильный путь. Всё заработало на ура. Удивительно, но оказалось, что в модели Post_Model оказалось не нужным описывать связь has_one с моделью Topic_Model, все прекрасно работает и без нее.
Все логично — откуда пост может знать, что он последний в каком-либо топике? Ему это и не положено.
>>> Все логично — откуда пост может знать, что он последний в каком-либо топике? Ему это и не положено.
Я думаю что у Random не выдало ошибку поскольку он получал посты через топик. А вот если бы попробывал получить топик через пост то эти поля понадобились бы.
Большое спасибо за статью!
У меня есть вопрос. Стандарстная база для топиков и тегов к ним. Много к многому. На ORM v 2 я делал так когда хотел получить все топики определенного тега
$this->join(‘news_tags’, ‘news_tags.news_id’, ‘news.id’);
$this->where(‘news_tags.tag_id’, $form['tag_id']);
сейчас эта штука не проходит. Запрос не верно генерируется. Не подскажите в чем проблема и как ее решить?
У поста и так есть топик-владелец
Попробуйте
$this->join('news_tags')->on('news_tags.news.id', '=', 'news.id')->where(’news_tags.tag_id’, '=', $form['tag_id']);
Только что покопался в исходниках и нашел решение. Зашел что бы поделится. И тут смотрю вы уже ответили. За что вам большое спасибо!
Еще один вопрос в догонку. kohana нормально обрабатывает суффиксы таблиц если их названия использовать как я использовал …->on(‘news_tags.news.id’, ‘=’, ‘news.id’) ?
Названия таблиц — без проблем. А вот если будете создавать модель для таблицы news_tags, имя файла должно быть classes/model/news/tag.php. Это связано с преобразованием подчеркиваний в слэши, что и привело к появлению поддиректории news.
Ясно. Спасибо.
Возник еще один вопрос. Я делаю регистрацию. Правила валидации прописываю в моделе так
protected $_labels = array(
‘password’ => ‘Пароль’,
‘r_password’ => ‘Пароль повторно’,
);
protected $_rules = array(
‘password’ => array(
‘not_empty’ => NULL,
‘min_length’ => array(4),
‘max_length’ => array(255),
),
‘r_password’ => array(
‘matches’ => array(‘password’),
),
);
А ошибку выдет: Поле Пароль повторно должно совпадать с password
Поле password не заменяется его лейблом. Что я не так сделал?
А и что еще самое интересное оно все время выдет что пароль и пароль повторно не совпадают =) Так что я точно что-то не то сделал.
Прошу прощения за такой навал комментариев. Но уточнить ошибку нужно. r_password все время пустое. И понятно почему. ORM его не загружает. Поому и совпадения не происходит паролей. Но проверку то нужно сделать =) Я запутался.
1. У Вас в модели есть строчка
? Если поля нет в таблице, надо его объявить в $_ignored_columns.
2. Судя по всему, проблема с «непереводом» текста ошибки требует изменения кода фреймворка. Могу конечно ошибаться, но лейблы используются только для имени поля. Скорее всего потому, что обычно текст ошибки содержит значения полей, а не имена (кроме правила matches я других примеров не могу вспомнить).
Спасибо. На счет $_ignored_columns вы совершенно правы. С этим я разобрался. Но писать 4-е сообщение было уж совсем неудобно =)
На счет ошибок. Жаль. Прийдется искать решение.
И опять вопрос. Допустим мне нужно получить сумму цен всех товаров. Если использовать что-то вроде такого
ORM::factory(‘order’)->select(‘SUM(price) as price’)->…
То система закавычиивает SUM(price) и выдается ошибка SQL.
Пока что решил вопрос так
DB::query(Database::SELECT, ‘SELECT COUNT(*) as count FROM orders’)->as_object()->execute()->count;
Но решение не нравится. Использовать его и ORM как то не очень.. Раньше можно было передавать FALSE во второй парамерт select. Сейчас не проходит.
Сделайте метод get_prices(), в котором что-то вроде
А как передать параметры в callback функцию?
Например есть такой код
$post->add_callbacks(‘card_numer’, array($this, ‘exist’));
И функция:
public function exist(Validation $post)
{
…
}
Как можно передать параметр в эту функцию? например чтобы было
public function exist(Validation $post, $id)
{
…
}
Тут что-то надо поменять
$post->add_callbacks(‘card_numer’, array($this, ‘exist’));
а вот что незнаю.
Подскажите пожалуйста.
Насколько я знаю, параметры могут быть переданы только в правила (add_rule()). А зачем Вам передавать идентификатор? Доступ к текущим значениям полей можно получить через $this.
В пост массиве передается номер карточки клиента(после редактирования) и надо проверить нет ли уже такого номера, но с учетом того что если нашелся такой номер, но он и есть у этого клиента, то не считать это ошибкой.
Сейчас я так проверял:
То есть если есть хоть одна карточка с таким номером — ошибка.
Но надо еще передать айди клиента и проверить если это номер текущего клиента — значит это не ошибка.
Номер текущего клиента можно получить как $this->id. Можно добавить дополнительное условие where() в запрос, чтобы отсеять текущую запись (только предварительно проверьте свойство $this->_loaded, т.к. модель может быть не сохранена в БД и id будет незаполнен).
Это все в контроллере происходит.
Есть идея сделать свойство у контроллера, куда записывать это айди перед добавлением калбека, а калбек-функции брать это значение.
Дык зачем все в контроллере делать? Валидация должна быть в модели, сами видите, какие проблемы возникают. Не надо костыли плодить.
Уже нету времени, проект горит, потом переделаю.
Раз уж тут развернулась такая активная дискуссия, добавлю свои пять копеек. Хочу выделять цветом сообщения, написанные модераторами, но очень не хочется создавать как отдельный запрос на каждый пост, так и хранить в посте не только id пользователя, но и его роль. Как бы так хитро сформировать запрос, используя ORM, чтобы для всех постов в топике получить роли их авторов (для авторизации используется модуль Auth)?
Как по мне, проще где-нить в кэше хранить идентификаторы модераторов и админов (их ведь немного), и сравнивать с ид текущего пользователя
Здорово, спасибо огромное! Статья очень помогла.
Kohana редкое говно, не идет не в какое сравнение с Yii, пишу сейчас приложение на Ko3.2, из исходников не вылажу, документация отсутствует.
Документации достаточно для старта. А внутренности фреймворка любой уважающий себя программист должен знать. ИМХО, одного проекта достаточно, чтобы в дальнейшем пользоваться в основном подсказками IDE.
Впрочем, никто никого не уговаривает, дело вкуса.
Дело даже не только в документации, фреймворк сам по себе очень слабенький, приходится много рутины писать самому, вместо того, чтобы сосредоточиться на основной задаче.
Конечно лучше чем никакого фреймворка, но все равно очень слабенько
> А внутренности фреймворка любой уважающий себя программист должен знать.
Вот внутренности Ruby on Rails или Zend Framework, за пару вечеров-то не осилишь.