关联:将模型连接在一起

CakePHP 的一个非常强劲的特性就是由模型提供关系映射,通过关联来管理多个模型间的连接。

在应用程序的不同对象间定义关系是很自然的。例如:在食谱数据库,一个食谱可能有多个评论,每个评论有一个作者,每个作者可能有多个评论。 以定义这些关系的形式工作,将允许你以一种直观且强大的方式访问你的数据库。

本节的目的是展示如何在 CakePHP 中计划、定义以及利用模型间的关系。

虽然数据可能来自各种源,但在 web 应用程序中最常见的则是存储在关系数据库中。 本节将覆盖这方面的大部分内容。

关于与插件模型一起的关联的信息,请参见 插件模型

关系类型

CakePHP 的关系类型有四种: hasOne、hasMany、belongsTo 和 hasAndBelongsToMany (HABTM)。

关系 关联类型 例子
     
一对多 hasMany 一个用户有多份食谱
多对一 belongsTo 多份食谱属于同一个用户
多对多 hasAndBelongsToMany 多份食谱有且属于多种成分

关联是通过创建一个由你定义的关联命名的类变量来定义的。 此变量有时候可能是简单的字符串,但也可能是用于定义关联细节的复杂的多维数组。

class User extends AppModel {
    public $hasOne = 'Profile';
    public $hasMany = array(
        'Recipe' => array(
            'className'  => 'Recipe',
            'conditions' => array('Recipe.approved' => '1'),
            'order'      => 'Recipe.created DESC'
        )
    );
}

在上面的例子中,第一个实例的单词 ‘Recipe’ 是别名。它是关系的唯一标识,它可以是你选择的任何东西。通常你会选择与要引用的类相同的名字。然而,每个模型的别名在应用程序中必须唯一。合适的例子有:

class User extends AppModel {
    public $hasMany = array(
        'MyRecipe' => array(
            'className' => 'Recipe',
        )
    );
    public $hasAndBelongsToMany => array(
        'MemberOf' => array(
            'className' => 'Group',
        )
    );
}

class Group extends AppModel {
    public $hasMany = array(
        'MyRecipe' => array(
            'className'  => 'Recipe',
        )
    );
    public $hasAndBelongsToMany => array(
        'Member' => array(
            'className' => 'User',
        )
    );
}

但是在所有的情况下,以下代码都不工作:

class User extends AppModel {
    public $hasMany = array(
        'MyRecipe' => array(
            'className' => 'Recipe',
        )
    );
    public $hasAndBelongsToMany => array(
        'Member' => array(
            'className' => 'Group',
        )
    );
}

class Group extends AppModel {
    public $hasMany = array(
        'MyRecipe' => array(
            'className'  => 'Recipe',
        )
    );
    public $hasAndBelongsToMany => array(
        'Member' => array(
            'className' => 'User',
        )
    );
}

因为在 HABTM 关联中,别名 ‘Member’ 同时指向了 User 模型(在 Group 模型中)和 Group 模型(在 User 模型中)。 在不同的模型为某个模型起不唯一的别名,可能会带来未知的行为。

Cake 能自动在关联模型对象间建立连接。所以你可以在你的 User 模型中以如下方式访问 Recipe 模型:

$this->Recipe->someFunction();

同样的,你也能在控制器中循着模型关系访问关联模型:

$this->User->Recipe->someFunction();

注解

记住,关系定义是 ‘单向的’。如果你定义了 User hasMany Recipe,对 Recipe 模型是没有影响的。你需要定义 Recipe belongsTo User才能从 Recipe 模型访问 User 模型。

hasOne

让我们设置 User 模型以 hasOne 类型关联到 Profile 模型。

首先,数据库表需要有正确的主键。对于 hasOne 关系,一个表必须包含指向另一个表的记录的外键。在本例中,profiles 表将包含一个叫做 user_id 的列。基本模式是: :

hasOne: 另一个 模型包含外键。

关系 结构
Apple hasOne Banana bananas.apple_id
User hasOne Profile profiles.user_id
Doctor hasOne Mentor mentors.doctor_id

注解

关于这一点,并没有强制要求遵循 CakePHP 约定,你能够很容易地在关联定义中使用任何外键来覆盖它。虽然如此,遵守规则将使你的代码更简捷,更易于阅读和维护。

User 模型文件保存为 /app/Model/User.php。为了定义‘User hasOne Profile’ 关联,需要在模型类中添加 $hasOne属性。记得要在 /app/Model/Profile.php 文件中放一个 Profile 模型,否则关联将不工作:

class User extends AppModel {
    public $hasOne = 'Profile';
}

有两种途径在模型文件中描述此关系。简单的方法是设置一个包含要关联的模型的类名的字符串型属性 $hasOne,就像我们上面做的那样。

如果需要更全面的控制,可以使用数组语法定义关联。例如,你可能想要限制关联只包含某些记录。

class User extends AppModel {
    public $hasOne = array(
        'Profile' => array(
            'className'    => 'Profile',
            'conditions'   => array('Profile.published' => '1'),
            'dependent'    => true
        )
    );
}

hasOne 关联数组可能包含的键有: :

  • className: 被关联到当前模型的模型类名。如果你定义了 ‘User hasOne Profile’关系,类名键将是 ‘Profile.’
  • foreignKey: 另一张表中的外键名。如果需要定义多个 hasOne 关系,这个键非常有用。其默认值为当前模型的单数模型名缀以 ‘_id’。在上面的例子中,就默认为 ‘user_id’。
  • conditions: 一个 find() 兼容条件的数组或者类似 array(‘Profile.approved’ => true) 的 SQL 字符串.
  • fields: 需要在匹配的关联模型数据中获取的列的列表。默认返回所有的列。
  • order: 一个 find() 兼容排序子句或者类似 array(‘Profile.last_name’ => ‘ASC’) 的 SQL 字符串。
  • dependent: 当 dependent 键被设置为 true,并且模型的 delete() 方法调用时的参数 cascade 被设置为 true,关联模型的记录同时被删除。在本例中,我们将其设置为 true 将导致删除一个 User 时同时删除与其相关的 Profile。

一旦定义了关系,User 模型上的 find 操作将匹配存在的关联 Profile 记录:

// 调用 $this->User->find() 的示例结果。

Array
(
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
    [Profile] => Array
        (
            [id] => 12
            [user_id] => 121
            [skill] => Baking Cakes
            [created] => 2007-05-01 10:31:01
        )
)

belongsTo

现在我们有了通过访问 User 模型获取相关 Profile 数据的办法,让我们在 Profile 模型中定义 belongsTo 关联以获取相关的 User 数据。belongsTo 关联是 hasOne 和 hasMany 关联的自然补充:它允许我们从其它途径查看数据。

在为 belongsTo 关系定义数据库表的键时,遵循如下约定:

belongsTo: 当前模型 包含外键。

关系 结构
Banana belongsTo Apple bananas.apple_id
Profile belongsTo User profiles.user_id
Mentor belongsTo Doctor mentors.doctor_id

小技巧

如果一个模型(表)包含一个外键,它 belongsTo 另一个模型(表)。

我们可以使用如下字符串语法,在 /app/Model/Profile.php 文件中的 Profile 模型中定义 belongsTo 关联:

class Profile extends AppModel {
    public $belongsTo = 'User';
}

我们还能使用数组语法定义特定的关系:

class Profile extends AppModel {
    public $belongsTo = array(
        'User' => array(
            'className'    => 'User',
            'foreignKey'   => 'user_id'
        )
    );
}

belongsTo 关联数组可能包含的键有:

  • className: 被关联到当前模型的模型类名。如果你定义了 ‘Profile belongsTo User’关系,类名键的值将为 ‘User.’

  • foreignKey: 当前模型中需要的外键。用于需要定义多个 belongsTo 关系。其默认值为另一模型的单数模型名缀以 ‘_id’。

  • conditions: 一个 find() 兼容条件的数组或者类似 array('User.active' => true) 的 SQL 字符串。

  • type: SQL 查询的 join 类型,默认为 Left,这不可能在所有情况下都符合你的需求,在你想要从主模型和关联模型获取全部内容或者什么都不要时很有用!(仅在某些条件下有效)。 (注:类型值必须是小写,例如:left, inner)

  • fields: 需要在匹配的关联模型数据中获取的列的列表。默认返回所有的列。

  • order: 一个 find() 兼容排序子句或者类似 array('User.username' => 'ASC') 的 SQL 字符串。

  • counterCache: 如果此键的值设置为 true,当你在做 “save()” 或者 “delete()” 操作时关联模型将自动递增或递减外键关联的表的 “[singular_model_name]_count” 列的值。如果它是一个字符串,则其将是计数用的列名。计数列的值表示关联行的数量。也可以通过使用数组指定多个计数缓存,键为列名,值为条件,例如:

    array(
        'recipes_count' => true,
        'recipes_published' => array('Recipe.published' => 1)
    )
    
  • counterScope: 用于更新计数缓存列的可选条件数组。

一旦定义了关联,Profile 模型上的 find 操作将同时获取相关的 User 记录(如果它存在的话):

//调用 $this->Profile->find() 的示例结果。

Array
(
   [Profile] => Array
        (
            [id] => 12
            [user_id] => 121
            [skill] => Baking Cakes
            [created] => 2007-05-01 10:31:01
        )
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
)

hasMany

下一步:定义一个 “User hasMany Comment” 关联。一个 hasMany 关联将允许我们在获取 User 记录的同时获取用户的评论。

在为 hasMany 关系定义数据库表的键时,遵循如下约定:

hasMany: 其它 模型包含外键。

关系 结构
User hasMany Comment Comment.user_id
Cake hasMany Virtue Virtue.cake_id
Product hasMany Option Option.product_id

我们可以使用如下字符串语法,在 /app/Model/User.php 文件中的 User 模型中定义 hasMnay 关联:

class User extends AppModel {
    public $hasMany = 'Comment';
}

我们还能使用数组语法定义特定的关系:

class User extends AppModel {
    public $hasMany = array(
        'Comment' => array(
            'className'     => 'Comment',
            'foreignKey'    => 'user_id',
            'conditions'    => array('Comment.status' => '1'),
            'order'         => 'Comment.created DESC',
            'limit'         => '5',
            'dependent'     => true
        )
    );
}

hasMany 关联数组可能包含的键有:

  • className: 被关联到当前模型的模型类名。如果你定义了 ‘User hasMany Comment’关系,类名键的值将为 ‘Comment.’。
  • foreignKey: 另一张表中的外键名。如果需要定义多个 hasMany 关系,这个键非常有用。其默认值为当前模型的单数模型名缀以 ‘_id’。
  • conditions: 一个 find() 兼容条件的数组或者类似 array(‘Comment.visible’ => true) 的 SQL 字符串。
  • order: 一个 find() 兼容排序子句或者类似 array(‘Profile.last_name’ => ‘ASC’) 的 SQL 字符串。
  • limit: 想返回的关联行的最大行数。
  • offset: 获取和关联前要跳过的行数(根据提供的条件 - 多数用于分页时的当前页的偏移量)。
  • dependent: 如果 dependent 设置为 true,就有可能进行模型的递归删除。在本例中,当 User 记录被删除后,关联的 Comment 记录将被删除。
  • exclusive: 当 exclusive 设置为 true,将用 deleteAll() 代替分别删除每个实体来来完成递归模型删除。这大大提高了性能,但可能不是所有情况下的理想选择。
  • finderQuery: CakePHP 中用于获取关联模型的记录的完整 SQL 查询。用在包含许多自定义结果的场合。 如果你建立的一个查询包含关联模型 ID 的引用,在查询中使用 $__cakeID__$} 标记它。例如,如果你的 Apple 模型 hasMany Orange,此查询看上去有点像这样: SELECT Orange.* from oranges as Orange WHEREOrange.apple_id = {$__cakeID__$};

一旦关联被建立,User 模型上的 find 操作也将获取相关的 Comment 数据(如果它存在的话):

//调用 $this->User->find() 获得的结果示例。

Array
(
    [User] => Array
        (
            [id] => 121
            [name] => Gwoo the Kungwoo
            [created] => 2007-05-01 10:31:01
        )
    [Comment] => Array
        (
            [0] => Array
                (
                    [id] => 123
                    [user_id] => 121
                    [title] => On Gwoo the Kungwoo
                    [body] => The Kungwooness is not so Gwooish
                    [created] => 2006-05-01 10:31:01
                )
            [1] => Array
                (
                    [id] => 124
                    [user_id] => 121
                    [title] => More on Gwoo
                    [body] => But what of the Nut?
                    [created] => 2006-05-01 10:41:01
                )
        )
)

有件事需要记住:你还需要定义 Comment belongsTo User 关联,用于从两个方向获取数据。 我们在这一节概述了能够使你从 User 模型获取 Comment 数据的方法。在 Comment 模型中添加 Comment belongsTo User 关系将使你能够从 Comment 模型中获取 User 数据 - 这样的链接关系才是完整的且允许从两个模型的角度获取信息流。

counterCache - 缓存你的 count()

这个功能帮助你缓存相关数据的计数。模型通过自己追踪指向关联 $hasMany 模型的所有的添加/删除并递增/递减父模型表的专用整数列,替代手工调用 find('count') 计算记录的计数。

这个列的名称由列的单数名后缀以下划线和单词 “count” 构成:

my_model_count

如果你有一个叫 ImageComment 的模型和一个叫 Image 的模型,你需要添加一个指向 p_w_picpaths 表的新的整数列并命名为p_w_picpath_comment_count

下面是更多的示例:

模型 关联模型 示例
User Image users.p_w_picpath_count
Image ImageComment p_w_picpaths.p_w_picpath_comment_count
BlogEntry BlogEntryComment blog_entries.blog_entry_comment_count

一旦你添加了计数列,就可以使用它了。通过在你的关联中添加 counterCache 键并将其值设置为 true,可以激活 counter-cache:

class ImageComment extends AppModel {
    public $belongsTo = array(
        'Image' => array(
            'counterCache' => true,
        )
    );
}

自此,你每次添加或删除一个关联到 Image 的 ImageCommentp_w_picpath_comment_count 字段的数字都会自动调整。

你还可以指定 counterScope。它允许你指定一个简单的条件,通知模型什么时候更新(不更新)计数值,这依赖于你如何查看。

在我们的 Image 模型示例中,我们可以象下面这样指定:

class ImageComment extends AppModel {
    public $belongsTo = array(
        'Image' => array(
            'counterCache' => true,
            'counterScope' => array('Image.active' => 1) // only count if "Image" is active = 1
        )
    );
}