Исключить пустые результаты для отношения HABTM в CakePHP

Большая часть документации CakePHP, кажется, говорит вам, как фильтровать на основе конкретного результата отношения. Чего я не могу найти, так это того, как отфильтровать результаты, которые имеют отношения, которые не возвращают данные.

Например, возьмем типичный пример блога, в котором есть сообщения и теги. Теги есть и принадлежат многим сообщениям (HABTM). Для этого обсуждения предположим следующую структуру таблицы:

posts ( id, title )
tags ( id, name )
posts_tags ( post_id, tag_id )

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

Идеальный результирующий набор будет выглядеть примерно так (кавычки добавлены для форматирования):

Array (
    [0] => Array (
            [Tag] => Array (
                      [id] => 1
                      [name] => 'Tag1' )
            [Post] => Array (
                      [0] => Array (
                              [id] => 1
                              [title] => 'Post1' )
                      [1] => Array (
                              [id] => 4
                              [title] => 'Post4' ) )
    )
    [1] => Array (
            [Tag] => Array (
                      [id] => 4
                      [name] => 'Tag5' )
            [Post] => Array (
                      [0] => Array (
                              [id] => 4
                              [title] => 'Post4' )
                      [1] => Array (
                              [id] => 5
                              [title] => 'Post5' )
                      [2] => Array (
                              [id] => 6
                              [title] => 'Post6' ) )
    ) )

person Aaron K    schedule 26.10.2011    source источник
comment
этот пост, кажется, решает эту проблему; а для 1.3.   -  person Ross    schedule 26.10.2011


Ответы (2)


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

person Rob Wilkerson    schedule 26.10.2011
comment
Возможно, я что-то пропустил, но после проверки синтаксиса в ссылке я получаю массив, где да, у тегов есть сообщения, но тег появляется несколько раз (один раз для каждого связанного сообщения). Кроме того, заголовок поста не связан. Мысли? - person Aaron K; 26.10.2011

Следующее было протестировано с Cake 1.3.

Для начала вы, вероятно, захотите или уже сделали отношение HABTM, определенное в моделях для всех других обстоятельств, где это обычно применяется:

class Post extends AppModel {
    var $hasAndBelongsToMany = 'Tag';
}

class Tag extends AppModel {
    var $hasAndBelongsToMany = 'Post';
}

Согласно собственной документации Cake: [1]

В CakePHP некоторые ассоциации (belongsTo и hasOne) выполняют автоматические соединения для извлечения данных, поэтому вы можете создавать запросы для извлечения моделей на основе данных в связанной.

Но это не относится к ассоциациям hasMany и hasAndBelongsToMany. Вот где принудительное соединение приходит на помощь. Вам нужно только определить необходимые соединения для объединения таблиц и получения желаемых результатов для вашего запроса.

Исключение пустых результатов HABTM — один из таких случаев. В этом же разделе Книги тортов объясняется, как это сделать, но я не нашел слишком очевидным из прочтения текста, что результат достигается именно этим. В примере с Cake Book они используют путь \join Book -> BooksTag -> Tags вместо нашего Tag -> PostsTag -> Posts. Для нашего примера мы настроили бы его следующим образом в TagController:

$options['joins'] = array(
    array(
        'table'      => 'posts_tags',
        'alias'      => 'PostsTag',
        'type'       => 'INNER',
        'foreignKey' => false,
        'conditions' => 'PostsTag.tag_id = Tag.id'
    ),
    array(
        'table'      => 'posts',
        'alias'      => 'Post',
        'type'       => 'INNER',
        'foreignKey' => false,
        'conditions' => 'Post.id = PostsTag.post_id'
    )
);

$tagsWithPosts = $this->Tag->find('all', $options);

Убедитесь, что для параметра externalKey установлено значение false. Это говорит Cake, что ему не следует пытаться вычислить условие соединения, а вместо этого использовать только то условие, которое мы предоставили.

Обычно это возвращает повторяющиеся строки из-за характера соединений. Чтобы уменьшить возвращаемый SQL, при необходимости используйте DISTINCT для полей. Если вам нужны все поля, которые обычно возвращаются функцией find('all'), это усложняет необходимость жесткого кодирования каждого столбца. (Конечно, структура вашей таблицы не должна меняться так часто, но это может случиться, или если у вас просто много столбцов). Чтобы получить все столбцы программно, добавьте следующее перед вызовом метода find:

$options['fields'] = array('DISTINCT Tag.'
                   . implode(', Tag.', array_keys($this->Tag->_schema)));
// **See note

Важно отметить, что отношение HABTM запускается ПОСЛЕ основного выбора. По сути, Cake получает список подходящих тегов, а затем запускает еще один раунд операторов SELECT, чтобы получить связанные сообщения; вы можете увидеть это из дампа SQL. «Соединения», которые мы настраиваем вручную, применяются к первому выбору, что дает нам желаемый набор тегов. Затем снова запустится встроенный HABTM, чтобы предоставить нам ВСЕ связанные сообщения с этими тегами. У нас не будет никаких тегов, у которых нет сообщений, нашей цели, но мы можем получить сообщения, связанные с тегом, которые не являются частью какого-либо из наших первоначальных «условий», если они были добавлены.

Например, добавив следующее условие:

$options['conditions'] = 'Post.id = 1';

Будет получен следующий результат:

Array (
    [0] => Array (
            [Tag] => Array (
                      [id] => 1
                      [name] => 'Tag1' )
            [Post] => Array (
                      [0] => Array (
                              [id] => 1
                              [title] => 'Post1' )
                      [1] => Array (
                              [id] => 4
                              [title] => 'Post4' ) )
    )
)

Судя по выборке данных в вопросе, только Tag1 был связан с нашим заявлением об «условиях». Таким образом, это был единственный результат, возвращенный «соединениями». Однако, поскольку HABTM запустился после этого, он перехватил все сообщения (Post1 и Post4), которые были связаны с Tag1.

Этот метод использования явных соединений для получения желаемого начального набора данных также обсуждается в Краткий совет: выполнение нерегламентированных соединений в Model::find(). В этой статье также показано, как обобщить эту технику и добавить ее в AppModel, расширяющую find().

Если бы мы действительно хотели видеть только Post1, нам нужно было бы добавить 'contain'[2] опциональное предложение:

$this->Tag->Behaviors->attach('Containable');
$options['contain'] = 'Post.id = 1';

Давая результат:

Array (
    [0] => Array (
            [Tag] => Array (
                      [id] => 1
                      [name] => 'Tag1' )
            [Post] => Array (
                      [0] => Array (
                              [id] => 1
                              [title] => 'Post1' ) )
    )
)

Вместо использования Containable вы можете использовать bindModel для переопределения отношения HABTM с этим экземпляром find(). В bindModel вы должны добавить желаемое условие публикации:

$this->Tag->bindModel(array(
    'hasAndBelongsToMany' => array(
        'Post' => array('conditions' => 'Post.id = 1'))
    )
);

Я чувствую, что для новичков, пытающихся понять автоматические способности торта, создание явных соединений легче увидеть и понять (я знаю, что это было для меня). Другим допустимым и, возможно, более «тортовым» способом сделать это было бы использование исключительно unbindModel и bindModel. Teknoid на http://nuts-and-bolts-of-cakephp.com имеет хороший отчет о том, как это сделать: http://nuts-and-bolts-of-cakephp.com/2008/07/17/forcing-an-sql-join-in-cakephp/. Кроме того, teknoid превратил это в поведение, которое вы можете скачать с github: http://nuts-and-bolts-of-cakephp.com/2009/09/26/habtamable-behavior/

** Это вытянет столбцы в порядке, определенном в базе данных. Поэтому, если первичный ключ не определен первым, он может не применить DISTINCT, как ожидалось. Возможно, вам придется изменить это, чтобы использовать array_diff_key для фильтрации первичного ключа из $this->Model->primaryKey.

person Aaron K    schedule 27.10.2011