Как отфильтровать по переводам принадлежащих ToMany ассоциаций?

Я хотел бы запросить переводы в связанной с ним принадлежности. В соответствии с документами и этим вопросом должна быть возможность запрашивать в переводах ассоциации. Я попробовал следующий (упрощенный) код:

    $result = $this->table()->find()
        ->where([
            $this->Activities->Tags->translationField('name') . ' LIKE' => 
                '%' . $request->filter . '%'
            ])
        ->leftJoinWith('Tags')
        ->contain(['Tags'])
        ->all()
        ->toArray();

Теги и действия имеют отношение ко многим.

Мероприятия:

    $this->belongsToMany('Tags', [
        'foreignKey' => 'activity_id',
        'targetForeignKey' => 'tag_id',
        'joinTable' => 'activities_tags'
    ]);

    $this->addBehavior('Translate', ['fields' => ['name', 'description']]);

Метки:

    $this->belongsToMany('Activities', [
        'foreignKey' => 'tag_id',
        'targetForeignKey' => 'activity_id',
        'joinTable' => 'activities_tags'
    ]);

    $this->addBehavior('Translate', ['fields' => ['name']]);

ActivityTag:

    $this->belongsTo('Activities', [
        'foreignKey' => 'activity_id',
        'joinType' => 'INNER'
    ]);
    $this->belongsTo('Tags', [
        'foreignKey' => 'tag_id',
        'joinType' => 'INNER'
    ]);

Тем не менее, я получаю следующий сгенерированный SQL:

SELECT 
    ...
FROM `activities` `Activities` 
LEFT JOIN `activities_tags` `ActivitiesTags` ON `Activities`.`id` = (`ActivitiesTags`.`activity_id`) 
LEFT JOIN `tags` `Tags` ON `Tags`.`id` = (`ActivitiesTags`.`tag_id`) 
LEFT JOIN `i18n` `Activities_name_translation` ON (
    `Activities_name_translation`.`model` = :c0 
    AND `Activities_name_translation`.`field` = :c1 
    AND `Activities_name_translation`.`locale` = :c2 
    AND `Activities`.`id` = (`Activities_name_translation`.`foreign_key`)
) 
LEFT JOIN `i18n` `Activities_description_translation` ON (
    `Activities_description_translation`.`model` = :c3 
    AND `Activities_description_translation`.`field` = :c4 
    AND `Activities_description_translation`.`locale` = :c5 
    AND `Activities`.`id` = (`Activities_description_translation`.`foreign_key`)
) 
WHERE `Tags_name_translation`.`content` like :c6

Что приводит меня к следующей ошибке:

QLSTATE [42S22]: Столбец не найден: 1054 Неизвестный столбец "Tags_name_translation.content" в "где предложение"

Следующее соединение отсутствует:

LEFT JOIN `i18n` `Tags_name_translation` ON (
   `Tags_name_translation`.`model` = :c6 
    AND `Tags_name_translation`.`field` = :c7 
    AND `Tags_name_translation`.`locale` = :c8 
    AND `Tags`.`id` = (`Tags_name_translation`.`foreign_key`)
) 

Теперь мой вопрос / Правка:

Чего мне не хватает, чтобы настроить CakePHP для генерации недостающего соединения? Мое намерение состоит в том, чтобы отфильтровать действия по переведенным тегам. Это работает для непереводов.

1 ответ

Решение

Как упомянуто в связанном вопросе, так же, как для содержания hasMany ассоциации, belongsToMany ассоциации извлекаются в отдельном запросе, и именно здесь поведение "Translate" включается и содержит ассоциации перевода (каждое поле представлено отдельным hasOne ассоциация), так что таблица перевода будет объединена.

То же самое верно для *joinWith() а также *matching()в то время как он применяет соединения и условия к основному запросу, фактическое содержание ассоциации и связанные с ней переводы снова извлекаются в отдельном запросе, т. е. поведение Translate не будет задействовано в основном запросе, следовательно, таблица перевода не присоединяться. Можно назвать это недостатком ORM, может быть, какой-то хук для объединения / сопоставления был бы полезен в тех случаях, когда поведение может соответствующим образом изменять запросы, но пока такой вещи нет.

Таким образом, не слишком задумываясь, вы можете, например, использовать коррелированный подзапрос (да, я знаю, он может работать не слишком хорошо) в качестве условия фильтрации, то есть запрашивать требуемые теги через Tags таблица, в которую будут включены переводы, и использовать, например, EXISTS состояние на Activitiesчто-то вроде этого:

$tagsQuery = $this->Activities->Tags
    ->find()
    ->select(['id'])
    ->innerJoinWith('ActivitiesTags')
    ->where(function (\Cake\Database\Expression\QueryExpression $exp) use ($request)  {
        return $exp
            ->equalFields('ActivitiesTags.activity_id', 'Activities.id')
            ->like(
                $this->Activities->Tags->translationField('name'),
                '%' . $request->filter . '%'
            );
    });

$activitiesQuery = $this->Activities
    ->find()
    ->where(function ($exp) use ($tagsQuery) {
        return $exp->exists($tagsQuery);
    });

Как видно, это потребует объединения в таблицу соединений (ActivitiesTags) вручную (в более ранних версиях CakePHP вам может понадобиться добавить эту связь вручную IIRC), чтобы вы могли сопоставить ActivitiesTags.activity_id, Результирующий запрос должен выглядеть примерно так:

SELECT 
  `Activities`.`id` AS `Activities__id`, ...
FROM 
  `activities` `Activities` 
WHERE 
  EXISTS (
    SELECT 
      `Tags`.`id` AS `Tags__id` 
    FROM 
      `tags` `Tags` 
      INNER JOIN `activities_tags` `ActivitiesTags` ON
        `Tags`.`id` = (`ActivitiesTags`.`tag_id`) 
      LEFT JOIN `i18n` `Tags_name_translation` ON (
        `Tags_name_translation`.`model` = 'Tags' 
        AND `Tags_name_translation`.`field` = 'name' 
        AND `Tags_name_translation`.`locale` = 'en_US' 
        AND `Tags`.`id` = (`Tags_name_translation`.`foreign_key`)
      ) 
    WHERE 
      (
        `ActivitiesTags`.`activity_id` = (`Activities`.`id`) 
        AND `Tags_name_translation`.`content` LIKE '%foobarbaz%'
      )
  )

Скорее всего, есть другие способы решения этой проблемы, например, создание и удержание дополнительных ассоциаций перевода "вручную" в Model.beforeFind время. Посмотреть на то TranslateBehavior::setupFieldAssociations() а также TranslateBehavior::beforeFind() нужно применить что-то похожее на Activities стол для достижения этого.

Другие вопросы по тегам