Контент


Ko3 ORM. Изучаем заново?

Возвращаюсь к одной из самых популярных тем, связанных с фреймворком 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), но это все дело времени. В целом все совсем даже неплохо.

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.

Теги: , , , , .


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

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

  1. none пишет:

    Только решил перелезть с Ci на Ко и начал читать доку на их сайте, а тут они выпускают Ко3 с такими изменениями, что та документация становится неактуальной…
    так что пишите чаще и больше! (-: лично я буду благодарен (-:

  2. BIakaVeron пишет:

    К сожалению, не всегда удается выделить время и подобрать удачную тему… Но я над этим работаю. Спасибо, что читаете.
    По поводу документации:
    1. Многие секции документации (файловая система, отдельные хэлперы и библиотеки) остаются актуальными и для Ko3.
    2. Не забываем, что 2.3.4 никуда не делась, да и 2.4 будет все-таки ближе к ветке 2.3, чем к 3.0.
    PS. А с CI слезайте, у нас веселее ;)

  3. Random пишет:

    BIakaVeron, большое спасибо за статью. Только что «рыл» официальный форум, многих ответов так и не нашел. А тут все предельно понятно, разложено по полочкам.

    Ждем мануал про роуты, а то в статье на Хабре совсем не понятно, как же сделать какой-нибудь параметр необязательным.

  4. none пишет:

    CI подкупает своей документацией и простотой (-:
    Да ну понятно, что 2.х никуда не делась, но рано или поздно ветка закроется, а портировать на Ко3 будет не самая весёлая процедура…

  5. Sergei пишет:

    А как можно с помошью ORM выбрать всех пользователей у которых есть хотя бы одна стотья в блогах?

  6. BIakaVeron пишет:

    Самый простой вариант (не в плане формирования SQL-запроса, а в смысле логики) — это собрать пользователей в два шага:
    1. Получаем все id пользователей. Используем для этого Query Builder, так более практично.

    $ids = DB::select(new Database_Expression('DISTINCT `user_id`'))
                     ->from('blogs')
                     ->execute()
                     ->as_array('user_id', 'user_id');


    Чтобы не тащить все записи из таблицы, делаем выборку только поля `user_id`. Но чтобы еще больше сэкономить, добавим слово DISTINCT к запросу, так мы получим идентификаторы без повторов.
    2. Получаем пользователей на основании этих id.

    $users = ORM::factory('user')
                 ->where('id', 'IN', $ids)
                 ->find_all();

    Но более практичный (как мне кажется) вариант — это создание доп. таблицы типа user_stats, связанной с users через has_one — belongs_to. В нее можно складывать различные счетчики (количество посещений блога, количество статей и комментариев) и т.д. В таком случае пользователей можно будет получить следующим образом:

    $users = ORM::factory('user')
                  ->with('user_stats')
                  ->where('user_stats:blog_count', '>', 0)
                  ->find_all()


    Работоспособность последнего моего запроса подтвердить на все 100% не могу, но должно быть примерно так.

  7. Rafi B. пишет:

    @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

  8. BIakaVeron пишет:

    @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.

  9. Random пишет:

    Появился еще один вопрос про связи между таблицами и оптимизацию запросов. Для тем форума нужно получить дату и краткий текст последнего поста, ну и его автора. В таблице topics завел поле last_post_id, где хранится id последнего поста. Хотел использовать функцию with() для получения нужной мне записи, но связывание таблиц происходит по полям topic.id - post.topic_id, а не topic.last_post_id - post.id, хотя в моделях явно указал foregin_key и, на всякий случай, far_key:

    Topic_Model:

      // Relationships
      ...
      protected $_has_one = array(
        'last_post' => array(
          'model' => 'post',
          'far_key' => 'last_post_id',
          'foregin_key' => 'id'),
        );

    Post_Model:

      // Relationships
      protected $_belongs_to = array(
        'topic' => array(
          'model'       => 'topic',
          'foreign_key' => 'topic_id'),
        'last_post' => array(
          'model'       => 'topic',
          'far_key'     => 'last_post_id',
          'foregin_key' => 'id')
        );

    Наверно самым простым решением было бы создать дополнительную таблицу, где хранить информацию по топикам, но уж очень не хочется плодить лишние сущности.

  10. BIakaVeron пишет:

    Тут необходимо учитывать, что если мы храним идентификатор поста в топике, то получается отношение belongs_to (если смотреть со стороны топика). Так что:

    protected $_belongs_to = array(
    	"last_post"	=> array
    	(
    		"model" => "post",
    		"foreign_key" => "last_post_id"
    	),
    );

  11. Random пишет:

    Большое спасибо за наведение на правильный путь. Всё заработало на ура. Удивительно, но оказалось, что в модели Post_Model оказалось не нужным описывать связь has_one с моделью Topic_Model, все прекрасно работает и без нее.

  12. BIakaVeron пишет:

    Все логично — откуда пост может знать, что он последний в каком-либо топике? :) Ему это и не положено.

  13. Богдан пишет:

    >>> Все логично — откуда пост может знать, что он последний в каком-либо топике? :) Ему это и не положено.
    Я думаю что у Random не выдало ошибку поскольку он получал посты через топик. А вот если бы попробывал получить топик через пост то эти поля понадобились бы.
    Большое спасибо за статью!
    У меня есть вопрос. Стандарстная база для топиков и тегов к ним. Много к многому. На ORM v 2 я делал так когда хотел получить все топики определенного тега
    $this->join(‘news_tags’, ‘news_tags.news_id’, ‘news.id’);
    $this->where(‘news_tags.tag_id’, $form['tag_id']);
    сейчас эта штука не проходит. Запрос не верно генерируется. Не подскажите в чем проблема и как ее решить?

  14. BIakaVeron пишет:

    А вот если бы попробывал получить топик через пост то эти поля понадобились бы.

    У поста и так есть топик-владелец ;)

    У меня есть вопрос…

    Попробуйте $this->join('news_tags')->on('news_tags.news.id', '=', 'news.id')->where(’news_tags.tag_id’, '=', $form['tag_id']);

  15. Богдан пишет:

    Только что покопался в исходниках и нашел решение. Зашел что бы поделится. И тут смотрю вы уже ответили. За что вам большое спасибо!
    Еще один вопрос в догонку. kohana нормально обрабатывает суффиксы таблиц если их названия использовать как я использовал …->on(‘news_tags.news.id’, ‘=’, ‘news.id’) ?

  16. BIakaVeron пишет:

    Названия таблиц — без проблем. А вот если будете создавать модель для таблицы news_tags, имя файла должно быть classes/model/news/tag.php. Это связано с преобразованием подчеркиваний в слэши, что и привело к появлению поддиректории news.

  17. Богдан пишет:

    Ясно. Спасибо.

  18. Богдан пишет:

    Возник еще один вопрос. Я делаю регистрацию. Правила валидации прописываю в моделе так
    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 не заменяется его лейблом. Что я не так сделал?

  19. Богдан пишет:

    А и что еще самое интересное оно все время выдет что пароль и пароль повторно не совпадают =) Так что я точно что-то не то сделал.

  20. Богдан пишет:

    Прошу прощения за такой навал комментариев. Но уточнить ошибку нужно. r_password все время пустое. И понятно почему. ORM его не загружает. Поому и совпадения не происходит паролей. Но проверку то нужно сделать =) Я запутался.

  21. BIakaVeron пишет:

    1. У Вас в модели есть строчка

    protected $_ignored_columns = array('r_password');

    ? Если поля нет в таблице, надо его объявить в $_ignored_columns.
    2. Судя по всему, проблема с «непереводом» текста ошибки требует изменения кода фреймворка. Могу конечно ошибаться, но лейблы используются только для имени поля. Скорее всего потому, что обычно текст ошибки содержит значения полей, а не имена (кроме правила matches я других примеров не могу вспомнить).

  22. Богдан пишет:

    Спасибо. На счет $_ignored_columns вы совершенно правы. С этим я разобрался. Но писать 4-е сообщение было уж совсем неудобно =)
    На счет ошибок. Жаль. Прийдется искать решение.

  23. Богдан пишет:

    И опять вопрос. Допустим мне нужно получить сумму цен всех товаров. Если использовать что-то вроде такого
    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. Сейчас не проходит.

  24. BIakaVeron пишет:

    Сделайте метод get_prices(), в котором что-то вроде

    $result = DB::select(array('SUM("price")', 'price'))->from($this->_table)->execute($this->_db)->get('price');
  25. Евгений пишет:

    А как передать параметры в 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’));
    а вот что незнаю.
    Подскажите пожалуйста.

  26. BIakaVeron пишет:

    Насколько я знаю, параметры могут быть переданы только в правила (add_rule()). А зачем Вам передавать идентификатор? Доступ к текущим значениям полей можно получить через $this.

  27. Евгений пишет:

    В пост массиве передается номер карточки клиента(после редактирования) и надо проверить нет ли уже такого номера, но с учетом того что если нашелся такой номер, но он и есть у этого клиента, то не считать это ошибкой.
    Сейчас я так проверял:

     public function exist(Validation $post)
        {
     
            $c = ORM::factory('clientcard')->where('numer',$post->numer)->count_all();
            if($c>0)
                $post->add_error( 'card_numer', 'exist');
        }


    То есть если есть хоть одна карточка с таким номером — ошибка.
    Но надо еще передать айди клиента и проверить если это номер текущего клиента — значит это не ошибка.

  28. BIakaVeron пишет:

    Номер текущего клиента можно получить как $this->id. Можно добавить дополнительное условие where() в запрос, чтобы отсеять текущую запись (только предварительно проверьте свойство $this->_loaded, т.к. модель может быть не сохранена в БД и id будет незаполнен).

  29. Евгений пишет:

    Это все в контроллере происходит.
    Есть идея сделать свойство у контроллера, куда записывать это айди перед добавлением калбека, а калбек-функции брать это значение.

  30. BIakaVeron пишет:

    Дык зачем все в контроллере делать? Валидация должна быть в модели, сами видите, какие проблемы возникают. Не надо костыли плодить.

  31. Евгений пишет:

    Уже нету времени, проект горит, потом переделаю.

  32. Random пишет:

    Раз уж тут развернулась такая активная дискуссия, добавлю свои пять копеек. Хочу выделять цветом сообщения, написанные модераторами, но очень не хочется создавать как отдельный запрос на каждый пост, так и хранить в посте не только id пользователя, но и его роль. Как бы так хитро сформировать запрос, используя ORM, чтобы для всех постов в топике получить роли их авторов (для авторизации используется модуль Auth)?

  33. BIakaVeron пишет:

    Как по мне, проще где-нить в кэше хранить идентификаторы модераторов и админов (их ведь немного), и сравнивать с ид текущего пользователя

  34. Anastasia пишет:

    Здорово, спасибо огромное! Статья очень помогла.

  35. Никита пишет:

    Kohana редкое говно, не идет не в какое сравнение с Yii, пишу сейчас приложение на Ko3.2, из исходников не вылажу, документация отсутствует.

  36. biakaveron пишет:

    Документации достаточно для старта. А внутренности фреймворка любой уважающий себя программист должен знать. ИМХО, одного проекта достаточно, чтобы в дальнейшем пользоваться в основном подсказками IDE.

    Впрочем, никто никого не уговаривает, дело вкуса.

  37. Никита пишет:

    Дело даже не только в документации, фреймворк сам по себе очень слабенький, приходится много рутины писать самому, вместо того, чтобы сосредоточиться на основной задаче.
    Конечно лучше чем никакого фреймворка, но все равно очень слабенько :(

  38. Никита пишет:

    > А внутренности фреймворка любой уважающий себя программист должен знать.
    Вот внутренности Ruby on Rails или Zend Framework, за пару вечеров-то не осилишь.



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

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