排除CakePHP中HABTM关系的空结果

时间:2011-10-26 05:10:01

标签: cakephp cakephp-1.3 cakephp-appmodel

大多数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' ) )
    ) )

2 个答案:

答案 0 :(得分:1)

我发现以可靠方式执行此操作的唯一方法是使用ad hoc joins。使用这些,您可以指定内部联接类型并获得您想要的内容。

答案 1 :(得分:0)

使用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 - >标签,而不是我们的标签 - > PostsTag - >帖子。对于我们的示例,我们在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);

确保将foreignKey设置为false。这告诉Cake它不应该试图找出连接条件,而只使用我们提供的条件。

由于连接的性质,这会带来重复的行,这是很常见的。要减少返回的SQL,请根据需要在字段上使用DISTINCT。如果您希望find('all')通常返回所有字段,则会增加硬编码每列所需的复杂性。 (当然你的表结构不应该经常改变,但它可能发生,或者如果你可能只有很多列)。要以编程方式获取所有列,请在find方法调用之前添加以下内容:

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

值得注意的是,HABTM关系在主要选择中运行 AFTER 。从本质上讲,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在此之后运行,它抓取了与Tag1相关的所有帖子(Post1和Post4)。

Quick Tip - Doing Ad-hoc Joins in Model::find()中还讨论了使用显式连接获取所需初始数据集的方法。本文还介绍了如何推广该技术并将其添加到扩展find()的AppModel。

如果我们真的只想看Post1,我们需要添加一个'contains'[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' ) )
    )
)

您可以使用bindModel重新定义与此find()实例的HABTM关系,而不是使用Containable。在bindModel中,您将添加所需的Post条件:

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

我觉得初学者试图围绕蛋糕的自动化能力,让明确的联接更容易看到和理解(我知道这对我来说)。另一个有效且可以说更“蛋糕”的方法是使用unbindModel和bindModel。 http://nuts-and-bolts-of-cakephp.com上的Teknoid对如何执行此操作有很好的说明: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中过滤出主键。