關聯:將模型連接在一起
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 的模型,你需要添加一個指向 images 表的新的整數列並命名爲image_comment_count。
下面是更多的示例:
模型 | 關聯模型 | 示例 |
---|---|---|
User | Image | users.image_count |
Image | ImageComment | images.image_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 的 ImageComment,image_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
)
);
}