Контент


ORM. Коротко и ясно

Один из наиболее часто задаваемых вопросов на форуме касается использования Object Relational Mapping (коротко — технология преобразования объектов в реляционные модели БД). Работа с БД всегда актуальна, поэтому любые способы ее облегчить/ускорить/стандартизировать достойны внимания.

Актуально ли?

Для начала определимся с основными целями ORM. С одной стороны, необходимо организовать работу с БД, учитывая различные варианты взаимоотношения реляционных таблиц («один-к-одному», «один-ко-многим», «много-ко-многим») и необходимость объединения данных разных таблиц в одну выборку (или наоборот, сохранения данных одного объекта в несколько таблиц). С другой — все это должно быть максимально прозрачно и просто для программиста. Можно использовать возможности Query Builder‘а, но в этом случае приходится вручную объединять таблицы, да и читается такой код не очень.

Пример 1. Выбрать информацию о статье с id=1, ее авторе и комментариях к ней.
Query Builder:

    $db = new Database();
    $article = $db->getwhere('articles', array('id'=>4))->current();
    $comments = $db->getwhere('comments', array('article_id'=>4));
    $author = $db->getwhere('users', $article->author_id);

ORM:

    $article = ORM::factory('article', 4);
    $comments = $article->comments;
    $author = $article->author;

Пример 2. Добавить статью, вывести ее id.
Query Builder:

    $db = new Database();
    $res = $db->insert('articles', array
          (
          'title'=>'new article',
          'author_id'=>1,
          'text'=>'article text',
          'category_id'=>1,
          )
    );
    echo $res->insert_id();

ORM:

    $article = ORM::factory('article');
    $article->title = 'new article';
    $article->author_id = 1;
    $article->text = 'article text';
    $article->category_id = 1;
    $article->save();
    echo $article->id;

Согласитесь, ORM выглядит намного более естественно. Поля таблицы доступны как свойства объекта, связанные через внешние ключи таблицы можно получить как свойства объекта, редактирование записи таблицы происходит «вживую», прямо над объектом. Однако все это само собой не появится.

Строим ORM

Вы могли заметить, что в приведенных выше примерах создавался некий объект (ORM::factory(‘article’)). Это экземпляр модели Article_Model, в которой и необходимо реализовать используемые в дальнейшем способы работы с БД. Звучит страшно, но на самом деле большая часть работы уже проделана в классе ORM (system/libraries/ORM.php), который Article_Model расширяет. ORM реализует паттерн Active Record, описывающий методы взаимодействия объекта и записей в БД. Нам в принципе остается только построить схему взаимоотношений таблиц, чтобы ORM-классы могли с ее помощью извлекать данные. Поэтому зачастую модели состоят только из перечисления внешних ключей.

Модель «статья»:

<?php defined('SYSPATH') OR die('No direct access allowed.');
 
class Article_Model extends ORM {
 
protected $has_one = array('author');
protected $has_many = array('comments');
protected $has_and_belongs_to_many = array('categories');
 
}

Свойства носят говорящие названия, по описанию модели понятно, что на одну статью приходится один автор и несколько (т.е. от нуля до бесконечности) комментариев. Для того, чтобы можно было отследить связи в обратную сторону, создаем модели для авторов и комментариев.

Модель «автор»:

<?php defined('SYSPATH') OR die('No direct access allowed.');
class Author_Model extends ORM {
 
    protected $has_many = array('articles');
}

Модель «комментарии»:

<?php defined('SYSPATH') OR die('No direct access allowed.');
class Comment_Model extends ORM{
 
protected $belongs_to = array('article');
}

Существует 4 вида связей, они описывают одностороннее взаимодействие (т.е. то, как текущая модель относится к другим):

  • has_one — модель связана с одной моделью (связь «один-к-одному»), например статья-автор.
  • has_many — модель может содержать несколько других моделей (связь «один-ко-многим»), например статья-комментарии.
  • belongs_to — модель является частью другой модели (обратная сторона связи «один-ко-многим»), комментарии-статья.
  • has_and_and_belongs_to_many — модель может быть включена в другие модели, но и сама может содержать несколько таких моделей. Например, использованные нами категории статей связаны со статьями связью «много-ко-многим».

Так как таблица может быть связана с другой по нескольким полям, можно использовать псевдонимы:

$has_one = array('author'=>'user', 'redactor'=>'user');


В этом случае поля $article->author и $article->redactor будут объектами User_Model, а таблица users связана с таблицей articles через внешние ключи author_id и redactor_id.

Учтите, что такое удобство использования взамен ставит перед нами некоторые ограничения:

  • Имена моделей в единственном числе (Comment_Model, а не Comments_Model).
  • Имена таблиц во множественном числе (Users, а не User). Может быть отменено установкой свойства модели $table_names_plural равным FALSE, но ведь лучше придерживаться правил ;)
  • Первичный ключ должен называться id и быть автоинкрементным. Конечно, это не всегда возможно, поэтому можно установить имя ключа в свойстве $primary_key (так мы сделали в статье «Представьтесь» с моделью User_Detail_Model, которая была связана с User_Model связью «один-к-одному»).
  • Имена внешних ключей образуются путем добавления суффикса _id к имени соответствующей модели (author_id для указания автора статьи).
  • Для связей много-ко-многим в реляционных БД используют т.н. «слабые сущности» — это таблицы, состоящие из первичных ключей связанных таблиц (например, для связи категорий и статей придется создать таблицу с полями article_id и category_id). Имена этих таблиц должны состоять из имен связанных таблиц, разделенных знаком подчеркивания (имена ставятся по алфавиту), в случае с категориями и статьями получим таблицу articles_categories.

Вообще стоит обратить внимание на случаи, когда используются термины в множественном числе, а когда в единственном. Во множественном числе пишутся названия таблиц (users, articles, comments) и названия полей при указании связи «много-ко-многим» и «один-ко-многим» ($has_many = array(‘comments’);, $has_and_belongs_to_many = array(‘categories’);). ORM определяет множественные и единственные числа с помощью хэлпера Inflector, поэтому если возникнут проблемы с названиями таблиц или связей можете смело смотреть, какой результат вернет inflector::plural() или inflector::singular().

Методы ORM

Давайте посмотрим, что можно сделать с ORM.
Создание. Два варианта — конструктор new или через ORM::factory(). Принципиально разницы никакой:

$article = new Article_Model();
$article = ORM::factory('article');

Также можно в качестве дополнительного параметра указать значение первичного ключа, чтобы модель создалась с учетом какой-либо записи в БД:

$article = new Article_Model(1);
$article = ORM::factory('article', 1);

Также можно создать пустой объект без указания id и затем загрузить в него какую-либо запись в таблице:

$article = ORM::factory('article');
$article->find(1);

Для указания условий выборки можно использовать методы Query Builder‘а:

// выбираем одну статью с author_id=1
$article = ORM::factory('article')->where('author', 1)->find();
// выбираем 10 первых статей с author_id=1
$article = ORM::factory('article')->where('author', 1)->find_all(10);

Если объекту соответствует один экземпляр другой таблицы, можно использовать метод with() для объединения таблиц при получении объекта. Например, загрузить информацию о статье (article) вместе с информацией об авторе (author):

$article = ORM::factory('article', 10)->with('author')->find();

После создания модели становятся доступны поля соответствующей таблицы через свойства объекта:

echo $article->id;
$article->title .= ' test';

Связанные таблицы также доступны через свойства, и так как они в свою очередь являются экземплярами моделей ORM, к ним также можно применять методы ORM и Query Builder‘а:

foreach ($article->comments as $comment)
    echo $comment->author->name;

Изменив какие-либо поля таблицы, можно их сохранить с помощью метода save(). При этом в объект заново загружается запись из БД (чтобы заполнить автоинкрементный ключ и поля, значения которых указаны по умолчанию).

    $article = new Article_Model(10);
    $article->title = 'Article #10';
    $article->author->name = 'test author';
    $article->save();
    $article->author->save();

Обратите внимание, что в таблице authors изменения не вступят в силу, если сделать только $article->save(). Необходимо вызывать save() для всех моделей, в таблицы которых мы хотим внести изменения. В случае со связями «много-ко-многим» все чуть-чуть сложнее:

    $article = new Article_Model(10);
    $article->add(ORM::factory('category', 1);

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

Опубликовано в cправочник.

Теги: , , , .


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

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

  1. Slaver пишет:

    ORM удобен, но невсегда. При сложных запросах формировать цепочки зависимостей практически невозможно.
    По мне идеальный вариант что-нибудь типа DbSimple. Надо будет подумать, как прикрутить его к Kohana.

  2. BIakaVeron пишет:

    Как говорится, Кесарю — кесарево ;)
    Иногда действительно проще сделать $this->db->query();

  3. ruX пишет:

    Здравствуйте.
    действительно коротко и ясно, спасибо — многие вещи оказались очень простыми.
    Вот только по отношению многие-ко-многим не очень понял.
    допустим есть таблицы movie(id, caption, year) и genre(id, genre)
    Как тогда поступить?
    Третья таблица movie_genre(movie_id, genre_id).
    А как это объяснить ORM’у?

    В Вашем примере
    $article = new Article_Model(10);
    $article->add(ORM::factory(‘category’, 1);
    вообще не очевидно что происходит… :?

  4. BIakaVeron пишет:

    Согласно конвенции ORM, промежуточная таблица должна называться genres_movies (т.к. буква g находится по алфавиту до буквы m). Также не забывайте, что имена таблиц должны быть во множественном числе.
    Метод add() предназначен для работы со связями много-ко-многим, в данном случае фактически он вставляет в промежуточную таблицу запись с article_id=10, category_id=1.

  5. Игорь пишет:

    Здравствуйте. Благодарю за описание. Немного прояснило ситуацию, а то я уже три дня ломаю голову, что это за зверь такой, ORM.

    Можно несколько вопросов?
    1. Правильно ли я понял, что на каждую таблицу должна быть создана своя модель?
    2. Модель создаётся путём прописывания в ней защищённых констант, которые объявляют ключи для связей с другими таблицами?
    3. Имена этих констант предопределены:
    has_one
    has_many
    belongs_to
    has_and_belongs_to_many ?
    4. Для того, чтобы эффективно работать с ORM БД должна быть спроектирована специальным образом?

    Если я всё правильно понял, то ещё вопросы:
    1. Если БД достаточно большая, и была спроектирована без учёта требований ORM, есть ли смысл притягивать сюда ORM?

    2. Моей стандартной практикой является написание хранимых процедур, через которые у меня идёт и выборка, и вставка, и редактирование, и удаление. Получается некий слой абстракции, да ещё и средствами СУБД можно следить за правами. Я не вижу выгод при отказе от своей практики, и переходе на ORM. И дело даже не в том, что на освоение ОRM уйдёт время, просто в хранимых функциях я могу использовать все средства, предоставляемые мне РСУБД.

  6. BIakaVeron пишет:

    1. Да
    2. Не констант, а свойств. Они могут быть переопределены у потомков.
    3. Данные свойства отвечают за связи модели с другими моделями. Есть еще дополнительные свойства, описывающие саму модель (например $table_name или $primary_key). Посмотрите статьи с тэгом «ORM», там все описано.
    4. Это желательно, но не всегда так уж необходимо. ORM (особенно в Ko3) достаточно гибок, можно указывать имена таблиц, первичных и внешних ключей и т.д.

    Доп. вопросы:
    1. См. ответ №4
    2. Насколько я знаю, хранимые процедуры не могут вызываться через методы объекта Database (поправьте меня, если ошибаюсь). Но не забывайте, что ORM по сути остается моделью, и в ней можно создавать свои методы для работы с СУБД, в том числе для вызова хранимых процедур.

  7. ruX пишет:

    Спасибо вам :!:
    Разобрался, во многом благодаря этому вашему посту
    http://brotkin.ru/2009/04/18/orm-svyazi/

  8. Игорь пишет:

    Хранимые процедуры вызываются посредством SQL запроса.
    Например, select * from Procedure_name(arg1, arg2, arg3). Так что можно вызвать хранимку через $db->query(). Да и можно в модели воспользоваться нативным PHP.

    pg_connect
    pg_query
    pg_fetchXXXX

    Даже получал в одном запросе курсор, фетчил его, и в другом запросе закрывал.

    Благодарю за ответы. Пойду ознакомлюсь с содержимым тега.

  9. Александр пишет:

    Ошибка:
    $article = new Article_Model(10);
    $article->add(ORM::factory(‘category’, 1);

    Надо:
    $article = new Article_Model(10);
    $article->add(ORM::factory(‘category’, 1));
    ;)

  10. demoriz пишет:

    http://www.redbeanphp.com
    интересный экземпляр ORM



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

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