Связи many to many в ORM

В транковой версии появилась полноценная поддержка связей типа many to many. Это значит, что теперь вся забота о поддержании, например, тегов в ваших плагинах ложится на ОРМ.

Использование

Для примера, пусть мы имеем топики и теги к ним. Одна статья может иметь несколько тегов, а один тег может содержать несколько статей. Причём как топики, так и теги имеют уникальный индекс. Для связей такого характера и используется тип many to many.

Подготовка базы данных
Для связей этого типа понадобится дополнительная таблица, в которой будут храниться связи между объектами. Она должна содержать два столбца — под id связываемых сущностей.
В нашем случае таблица может выглядеть так:
+----------+---------+
| Field    | Type    |
+----------+---------+
| topic_id | int(11) |
| tag_id   | int(11) |
+----------+---------+

Настройка сущностей

В приведённом выше примере используется две сущности — топик и тег.

EntityTopic.class.php
class EntityTopic extends EntityORM
{
    protected $aRelations = array(
        'tags' => array(self::RELATION_TYPE_MANY_TO_MANY,'EntityTag', 'tag_id', 'db.table.topic_tag_rel', 'topic_id')
    );
}

EntityTag.class.php
class EntityTag extends EntityORM
{
    protected $aRelations = array(
        'topics' => array(self::RELATION_TYPE_MANY_TO_MANY,'EntityTopic', 'topic_id', 'db.table.topic_tag_rel', 'tag_id')
    );
}

Рассмотрим объявление связи подробно и для этого возьмём строку из сущности топика, где объявляется связь с тегами:
'tags' => array(self::RELATION_TYPE_MANY_TO_MANY,'EntityTag', 'tag_id', 'db.table.topic_tag_rel', 'topic_id'

Тут:
[0] -> self::RELATION_TYPE_MANY_TO_MANY — тип связи
[1] -> 'EntityTag' — имя сущности объектов связи
[2] -> 'tag_id' — названия столбца в таблице связи, в котором содержатся id объектов связи, в нашем случае тегов.
[3] -> 'db.table.topic_tag_rel' — алиас (идентификатор из конфига) таблицы связи.
Обратите внмание на то, что ORM для определения таблиц сущностей использует модуль и название сущности, то есть если мы захотим таблицу связи назвать prefix_topic_tag, что, в общем-то, логично, то будет конфликт имён, потому что ModuleTopic_EntityTag также преобразуется в prefix_topic_tag.
Поэтому необходимо следить за корректным именованием таблиц (точнее алиасов в конфиге, сами таблицы в бд могут называться как угодно). В данном примере используется суффикс '_rel'.
[4] -> 'topic_id' — название столбца в таблице связи, в котором содержатся id сущности, для которой объявляется связь, в нашем случае топиков.

Добавление/удаление элементов
После того как сущности настроены, можно пользоваться заданной связью. Для этого возьмём объекты топика и несколько тегов (пусть топик создаётся, а теги уже существуют и загружаются) и свяжем их:
$oTopic = Engine::GetEntity('EntityTopic');
... // Тут идёт выставление полей топика, которое нас сейчас не интересует
$aTags = array(...); //Не важно, как мы их загрузили, главное. чтобы тут были объекты тегов
foreach($aTags as $oTag) {
    $oTopic->tags->add($oTag); // Добавление тега в коллекцию
}
$oTopic->add();


Основная строка здесь — это "$oTopic->tags->add($oTag);". Как видно, обращение к коллекции идёт через поле (не метод), совпадающее с именем связи.
На данный момент для коллекции поддерживаются два метода:
add($oObject), который добавляет указанный объект в коллекцию.
delete($iId)/delete($aIds), который удаляет объект (если в параметр передан единичный id) или объекты (если в параметр передан массив) из коллекции.
Все изменения в коллекции фиксируются в момент сохранения объекта.
В примере это:
$oTopic->add();
Но сказанное выше справедливо и для обновления с удалением.

Выборка
Для выборки используется метод $oTag->getTopics($aFilter), где topics — это имя связи. Параметром поддерживается фильтр.
Таким образом, если нам нужно получить по тегу все опубликованные топики, мы можем сделать так:
$aTopics = $oTag->getTopics(array('topic_publish' => 1));


Кэширование
Кэш работает, если включено общее по движку кэширование в конфиге. Если для какого-то конкретного запроса нужно изменить параметры кэширования, то в выборку по фильтру можно добавить параметр #cache. Это массив из трёх элементов, где
[0] — ключ кэша
[1] — тэги кэша
[2] — время жизни кэша

Для отключения кэширования запроса, можно вместо массива передать в запросе false:
$aTopics = $oTag->getTopics(array('topic_publish' => 1, '#cache' => false));

11 комментариев

avatar
Отличная новость, я как раз такую функцию искал с месяц назад для своего Плагина..:)

Если я не ошибаюсь то эта функция позволяет делать в одной таблице коллекцию не повторяющихся тегов или других элементов, а во второй привязку этих тегов ( элементов ) с нужным ID любой другой таблице.
avatar
Примерно так.
Есть таблица с топиками, есть таблица с тегами, есть промежуточная таблица со связями топик-тег. Соответственно получается, что любое количество топиков может быть связано с любым количеством тегов.
Например есть два топика с id равными 1 и 2, и три тега с id равными 1,2 и 3.
В таком случае записи в таблице связей могут выглядеть так:
topic_id | tag_id
       1 |      1
       1 |      3
       2 |      1
       2 |      2

Получается, что топиком 1 связано два тега — 1 и 3, в то же время, с тегом 1 связаны два топика — 1 и 2.
И если раньше такие свзяи нужно было поддерживать вручную — писать запросы для вставки, загрузки данных и т.п., то теперь достаточно выполнить манипуляции, описанные в статье.
avatar
А эта функция работает в не обновлённой версии LS 0.4.2 или только с последним обновлением дистрибутива..?
avatar
Это работает в транковой версии, как и остальной функционал ОРМ.
trac.lsdev.ru/svn/livestreet/trunk/
avatar
Спасибо за отличный мануал, ещё с ОРМ не разбирался в плотную но чувствую нужно начинать..:)
avatar
Маленький вопрос по ORM.
Допустим, я хочу, переделать стандартную схему tag->topic на новую (с использованием ORM).
Мне для этого придется менять предков EntityTopic и EntityTag на EntityORM?
avatar
да
avatar
Кроме этого нужно ещё будет модули перевести на ModuleORM.
Некоторую более подробную информацию можно почитать в этой статье.
avatar
А orm функциональность будет работать, если я свяжу топики со своей entity (плагин), которая не имеет маппера и вообще не работает с базой (данные хранятся в массиве в файле)?
avatar
А это зависит от реализации вашего плагина.
Самый простой вариант отвязки ОРМ от базы — это полное переписывание класса MapperORM, причём с сохранением всех методов. То есть вместо обращений к БД класс будет обращаться к файлам.
Потому что сущность обращается к модулю, модуль обращается к мапперу. Если не трогать интерфейсные методы маппера, то сущность и модуль про подмену ничего не узнают и их код можно будет оставить.
avatar
А как можно связать сущности ядра и новые сущности, созданные в плагине (например User и MyEntity)? Правильно ли я понимаю, что нужно создать сущность Userorm, наследуемую от User, и работать непосредственно с ней?

Может есть лучший способ?
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.