Рецепт: lazy loading relation with joined self model

Обсуждение документации. Переводы Cookbook и авторские рецепты.
Ответить
maxtorchel
Сообщения: 34
Зарегистрирован: 2013.11.19, 16:51

Рецепт: lazy loading relation with joined self model

Сообщение maxtorchel » 2014.06.25, 16:04

Гуглом не нашел решения, но додумал сам, решил поделиться.
Есть 2 модели - Bill и Score. В Score есть два поля для связи с другими моделями, по id (model_id) и по названию модели (model_name). Связь выглядит так:

Код: Выделить всё

 'bill' => array(self::BELONGS_TO, 'Bill', 'model_id', 'on'=>'model_name="Bill"') 
Тоесть модель Score привязана к модели Bill по model_id и model_name. Тут у модели Score может быть много связей, например Manifest

Код: Выделить всё

 'manifest' => array(self::BELONGS_TO, 'Manifest', 'model_id', 'on'=>'model_name="Manifest"') 
и тд. При этом id Bill и Manifest могут пересекаться поэтому условие on обязательно. Так вот если мы обращаемся к связи bill объекта Score ленивой загрузкой,

Код: Выделить всё

$score = Score::model()->findByPk(1);
$bill = $score->bill;
 
то нам выдаст ошибку, что поле model_name не найдено. Это происходит потому что при ленивой загрузке не применяется join, ищется просто в таблице bill по model_id и условие on подставляется в where. Таблица score с полем model_name в запросе не участвует.

Код: Выделить всё

select * from bill where id={model_id} and model_name="Bill" 
Здесь стоит отметить что данная проблема актуальна для всех случаев когда в связи есть доп условия для поиска (condition, order и тд) в которых участвуют поля из модели self, в данном случае Score. Никакие together=true или with не работают. Так как нам всетаки получить в запросе join двух таблиц? Для этого просто создадим прокладку из связи модели с самой собой, а имеющуюся связь прокинем через нее с помощью through. Назовем ее self:

Код: Выделить всё

 'self' => array(self::BELONGS_TO, 'Score', 'id'), //для того чтобы lazy loading работало с условием по полю model_name, так как при ней не работает join
'manifest' => array(self::BELONGS_TO, 'Manifest', array('model_id'=>'id'), 'through'=>'self', 'on'=>'self.model_name="Manifest"'),
'bill' => array(self::BELONGS_TO, 'Bill', array('model_id'=>'id'), 'through'=>'self', 'on'=>'self.model_name="Bill"'),
 
После таких изменений при выполнении кода

Код: Выделить всё

$bill = $score->bill; 
будет выполняться запрос

Код: Выделить всё

select * from bill bill join score self on self.model_id=bill.id where self.model_name="Bill" and self.id={id}   
что нам и требовалось.

gzhegow
Сообщения: 1
Зарегистрирован: 2015.01.11, 01:25

Re: Рецепт: lazy loading relation with joined self model

Сообщение gzhegow » 2015.01.11, 01:55

Не совсем понял исходные схемы и к концу окончательно уехала крыша.


Вопрос 1:
Как мне сделать теги для тегов?

Исходные условия - есть таблицы items, itemtags, tags

items - сущности
tags - теги
itemtags - связь сущностей и тегов

Но кроме этого в таблице tags есть поле itemid, которое связывает ее напрямую с item, поскольку в нашем случае tag также является сущностью, к которой можно привесить тег. Да, там крыша уезжает, можно бесконечную рекурсию замутить если захотеть.

Таким образом в модели Tags получается две связи на itemtags - первая для обычной связи, а вторая для связи through item, чтобы вытянуть теги тегов.

Вопрос в чем - как следить за конфликтом имен? Тупо создать связь itemtags и вторую itemtags2?

Есть ли способ удобнее?
скажем так "более стандартное решение", чтобы его принять как данность и всегда применять.
====

Вопрос 2:
Как работать с limit при together?
Я использую 1.1.16 и ActiveRecord, при помощи with.

Исходные условия те же, что и в Вопросе 1.
Если писать конструкцию вида:

Код: Выделить всё

Items::model()->with(array(
  'itemtags' => array(
    'with' => array(
      'tags' => array(
        'with' => array(
          'itemtags' => array(
              'alias' => 'tag_itemtags',
              'with' => array(
                'tags' => array(
                  'alias' => 'tag_tags'
                )
              )
            ),
          ),
        ),
      ),
    ),
  ),
))->find($criteria);
То в принципе все работает.
Я пытаюсь сократить код, создавая в модели связи с through, навроде:

Код: Выделить всё

'items'->'tags' through 'itemtags'
'tags'->'tags' through 'item' alias `tag_tags` 
И в принципе все работает до тех пор, пока не начинаются запросы с повторным использованием связей.

Пример такого запроса:
На сайте есть:

сущности Items
теги Tags
картинки Images
страницы Pages

Страницы, Картинки и Теги являются сущностями
Сущности могут иметь теги.

Где-то в этом месте пытаешься вытянуть все для всего, включая теги для тегов. Получается несколько раз использование связи itemtags, которая в модели написана один раз, и соответственно ее alias не меняется. Но даже если решить конфликт имен, проставив alias всем связям и убрать все through связи, то:

Ставишь together => true.
Получается единый запрос.

Но стоит к нему дописать limit, когда тебе нужно вытянуть например

Ровно 5
страниц с тегами и тегами тегов
с их картинками с тегами и тегами тегов
с их тегами с тегами тегов

, и начинается веселуха в том, что запись дублируется по числу картинок, тегов и всего чего под руку не попадет, а лимит работает четко, обрезает по числу записей, в итоге на выходе всего одна запись вместо пяти, если картинок было 5.

Убираешь together, и получается, что нужно CDbCriteria приходится писать для каждой связи заново.

Как это сделать удобнее?

Ответить