4.6 本地模型---间接“有多”条相关的外地记录

4.6.1 建立间接的“有多”关系

在我们的示例域中,每篇博文“有多”个标签,又因为每个标签“有多”篇博文,所以必须存在某张关联表来映射连接他们。这种“多对多”关系需要第三方模型的辅助;第三方模型的作用是映射本地和外地模型相互之间的关系。

因此,建立这种“多对多”关系比建立一般的“有多”或“属于”关系更复杂一些。首先你必须连接关联模型,然后通过关联模型连接外地模型。“多对多”关系的每一方都要如此操作。

<?php
class Acme_Model_Blogs extends Acme_Sql_Model
{
    protected function _setup()
    {
        // the "through" related association
        $this->_hasMany('taggings');
        
        // has many tags, through this relationship name
        $this->_hasManyThrough('tags', 'taggings');
    }
}

class Acme_Model_Tags extends Acme_Sql_Model
{
    protected function _setup()
    {
        // the "through" related association
        $this->_hasMany('taggings');
        
        // has many tags, through this relationship name
        $this->_hasManyThrough('blogs', 'taggings');
    }
}

最后,在关联模型中,指定它“属于”“多对多”关系的每一方。

<?php
class Acme_Model_Taggings extends Acme_Sql_Model
{
    protected function _setup()
    {
        $this->_belongsTo('blog');
        $this->_belongsTo('tag');
    }
}

4.6.2 延迟加载SQL

上面的代码在每条博客记录上创建了一个“虚假的”属性$tags,该属性从模型目录tags条目中执行延迟的数据查询。也就是说,每次你请求$blog_record->tags时,模型系统将会从数据库获取相关的归总记录并且放到$tags属性中。下面给出延迟加载SQL的一个示例:

SELECT tags.*
FROM tags AS tags
LEFT JOIN taggings AS taggings ON taggings.tag_id = tags.id
WHERE taggings.blog_id = {$blog_record->id}

类似地,上面的代码在每个标签记录上创建了一个“虚假的”属性$blogs,该属性从模型目录blogs条目中执行延迟的数据查询。也就是说,每次你请求$tag_record->blogs时,模型系统将会从数据库获取相关的归总记录并且放到$blogs属性中。下面给出延迟加载SQL的一个示例:

SELECT blogs.*
FROM blogs AS blogs
LEFT JOIN taggings AS taggings ON taggings.blog_id = blogs.id
WHERE taggings.tag_id = {$tag_record->id}
[Warning]延迟加载 VS. 贪婪加载

小心可怕的N+1问题。如果你获取一个包含10条博客记录的记录集并且遍历它们,每次打印出$blog_record->tags,你将生成额外的10条SELECT查询语句(每篇博文生成一条),那么这里总共就有11条查询语句了。为了避免这种情况发生,当你获取博客记录集时,使用贪婪方式获取相关的归总记录;要了解更多关于贪婪的内容,请看下节。

4.6.3 外键

在“间接有多”关系中,外键存在于“中间”关联模型/表中,并确定哪些映射是属于本地模型的。在我们的示例域中,通过taggings模型的连接,每篇博文有多个标签,这意味着博客的外键blog_id存在于taggings表中,并且映射blogs表的主键。类似地,通过taggings模型的连接,每个标签有多篇博文,标签的外键tag_id存在于taggings表中,并且映射tags表的主键。

4.6.4 中间键

在“间接有多”关系中,我们注意到还有第二个外键,“中间”键。“中间”键存在于“中间”模型/表中,并确定哪些映射是属于外地模型的。在我们的示例域中,通过taggings模型的连接,每篇博文有多个标签,“中间”键tag_id存在于taggings表中且映射tags表的主键。类似地,通过taggings模型的连接,每个标签有多篇博文,“中间”键tag_id存在于taggings表中且映射blogs表的主键。

4.6.5 关系定义

尽管Solar模型系统预先假设了数据表和外键的命名规则,但是这些假设都不是硬编码的。你可以在调用_hasManyThrough()方法时给它传递一个键-值数组来重新定义这种外部关系的方方面面。

<?php
        $this->_hasManyThrough('name', array(
            'option' => 'value',
            // ...
        ));
string foreign_name

正常情况下,模型系统期望外地模型以关系名称的复数形式存在于模型目录中。例如,本地的_hasOne('foreign')意味着外地模型名为‘foreigns’,并且关联了某些模型类。

使用foreign_name选项可以为外地模型设置一个不同的目录名。这允许你把关系名命为“foo”(也即记录的外键属性),却使用“bar”模型为其做相关的工作。foreign_class选项优先于foreign_name选项。

string foreign_key

正常情况下,模型系统期望外地表中的外键名和本地模型中foreign_col属性定义的值一致。

使用foreign_key选项可以在外地表中设置一个不同的字段名。(如果设置了native_colforeign_col选项,那么foreign_key选项将被忽略。)

array cols

为关系记录获取这些字段。

array conditions

获取关系记录时,对外地表附加的条件。这些在获取数据时,将被适当地使用为WHERE或是JOIN ON条件。(例如,延迟获取 VS. 贪婪获取)

array order

获取关系记录时附加的ORDER从句。

[Note]Note

以下是一些不太常见,但更高级和更细密的选项定义关系:

string foreign_class

外地模型的类名。默认是关系名在父类栈中匹配到的第一个类。

string foreign_alias

外地表的别名。默认是关系名。

string foreign_col

外地表中参与联合的字段名,与本地表中的一些字段进行匹配。这形成了关系条件的外地部分。

string native_col

本地表中参与联合的字段名,与外部表中的一些字段进行匹配。这形成了关系条件的本地部分。

string native_by

贪婪获取时用于连接本地记录的策略:‘wherein’,这意味着“WHERE IN (...)”(即“在”)本地ID的一个列表中;或者“select”,这意味着一个子查询。

int wherein_max

选择native-by策略时,使用‘wherein’最多在本地结果中产生这么多记录;之后,再使用‘select’策略。

string merge

指定为合并加入的外地记录的策略:‘server’是指数据库将通过单个SELECT语句把结果合并到本地获取的数据中(仅适合“对一”关系);然而,‘client‘是指使用PHP来处理它,在关系中使用一个额外的SELECT语句(“对多”关系总是如此,“对一”关系是可选的),之后在PHP循环中合并这些记录。