hasAndBelongsToMany (HABTM)
現在,你已經是 CakePHP 模型關聯的專家了。你已經深諳對象關係中的三種關聯。
現在我們來解決最後一種關係類型: hasAndBelongsToMany,也稱爲 HABTM。 這種關聯用於兩個模型需要多次重複以不同方式連接的場合。
hasMany 與 HABTM 主要不同點是 HABTM 中對象間的連接不是唯一的。例如,以 HABTM 方式連接 Recipe 模型和 Ingredient 模型。西紅柿不只可以作爲我奶奶意大利麪(Recipe)的成分(Ingredient),我也可以用它做色拉(Recipe)。
hasMany 關聯對象間的連接是唯一的。如果我們的 User hasMnay Comments,一個評論僅連接到一個特定的用戶。它不能被再利用。
繼續前進。我們需要在數據庫中設置一個額外的表,用來處理 HABTM 關聯。這個新連接表的名字需要包含兩個相關模型的名字,按字母順序並且用下劃線(_)間隔。表的內容有兩個列,每個外鍵(整數類型)都指向相關模型的主鍵。爲避免出現問題 - 不要爲這個兩個列定義複合主鍵,如果應用程序包含複合主鍵,你可以定義一個唯一的索引(作爲外鍵指向的鍵)。如果你計劃在這個表中加入任何額外的信息,或者使用 ‘with’ 模型,你需要添加一個附加主鍵列(約定爲 ‘id’)
HABTM 包含一個單獨的連接表,其表名包含兩個 模型 的名字。
關係 | HABTM 表列 |
---|---|
Recipe HABTM Ingredient | ingredients_recipes.id, ingredients_recipes.ingredient_id,ingredients_recipes.recipe_id |
Cake HABTM Fan | cakes_fans.id, cakes_fans.cake_id, cakes_fans.fan_id |
Foo HABTM Bar | bars_foos.id, bars_foos.foo_id, bars_foos.bar_id |
註解
按照約定,表名是按字母順序組成的。在關聯定義中自定義表名是可能的。
確保表 cakes 和 recipes 遵循了約定,由表中的 id 列擔當主鍵。如果它們與假定的不同,模型的 主鍵 必須被改變。
一旦這個新表被建立,我們就可以在模型文件中建立 HABTM 關聯了。這次我們將直接跳到數組語法:
class Recipe extends AppModel {
public $hasAndBelongsToMany = array(
'Ingredient' =>
array(
'className' => 'Ingredient',
'joinTable' => 'ingredients_recipes',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
HABTM 關聯數組可能包含的鍵有:
-
className: 關聯到當前模型的模型類名。如果你定義了 ‘Recipe HABTM Ingredient’ 關係,這個類名將是 ‘Ingredient.’
-
joinTable: 在本關聯中使用的連接表的名字(如果當前表沒有按照 HABTM 連接表的約定命名的話)。
-
with: 爲連接表定義模型名。默認的情況下,CakePHP 將自動爲你建立一個模型。上例中,它被稱爲 IngredientsRecipe。你可以使用這個鍵來覆蓋默認的名字。連接表模型能夠像所有的 “常規” 模型那樣用來直接訪問連接表。通過建立帶有相同類名和文件名的模型類,你可以向連接表搜索中加入任何自定義行爲,例如向其加入更多的信息/列。
-
foreignKey: 當前模型中需要的外鍵。用於需要定義多個 HABTM 關係。其默認值爲當前模型的單數模型名綴以 ‘_id’。
-
associationForeignKey: 另一張表中的外鍵名。用於需要定義多個 HABTM 關係。其默認值爲另一模型的單數模型名綴以 ‘_id’。
- unique: 布爾值或者字符串 keepExisting。
-
- 如果爲 true (默認值),Cake 將在插入新行前刪除外鍵表中存在的相關記錄。現有的關係在更新時需要再次傳遞。
- 如果爲 false,Cake 將插入相關記錄,並且在保存過程中不刪除連接記錄。
- 如果設置爲 keepExisting,其行爲與 true 相同,但現有關聯不被刪除。
-
conditions: 個 find() 兼容條件的數組或者 SQL 字符串。如果在關聯表上設置了條件,需要使用 ‘with’ 模型,並且在其上定義必要的 belongsTo 關聯。
-
fields: 需要在匹配的關聯模型數據中獲取的列的列表。默認返回所有的列。
-
order: 一個 find() 兼容排序子句或者 SQL 字符串。
-
limit: 想返回的關聯行的最大行數。
-
offset: 獲取和關聯前要跳過的行數(根據提供的條件 - 多數用於分頁時的當前頁的偏移量)。
-
finderQuery, deleteQuery, insertQuery: CakePHP 能用來獲取、刪除或者建立新的關聯模型記錄的完整 SQL 查詢語句。用在包含很多自定義結果的場合。
一旦關聯被創建,Recipe 模型上的 find 操作將可同時獲取到相關的 Tag 記錄(如果它們存在的話):
// 調用 $this->Recipe->find() 的結果示例。
Array
(
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Ingredient] => Array
(
[0] => Array
(
[id] => 123
[name] => Chocolate
)
[1] => Array
(
[id] => 124
[name] => Sugar
)
[2] => Array
(
[id] => 125
[name] => Bombs
)
)
)
如果在使用 Ingredient 模型時想獲取 Recipe 數據,記得在 Ingredient 模型中定義 HABTM 關聯。
註解
HABTM 數據被視爲完整的數據集。每次一個新的關聯數據被加入,數據庫中的關聯行的完整數據集被刪除並重新建立。所以你總是需要爲保存操作傳遞整個數據集。使用 HABTM 的另一方法參見 hasMany 貫穿 (連接模型)
小技巧
關於保存 HABTM 對象的更多信息請參見: 保存相關模型數據 (HABTM)
hasMany 貫穿 (連接模型)
有時候需要存儲帶有多對多關係的附加數據。考慮以下情況:
Student hasAndBelongsToMany Course
Course hasAndBelongsToMany Student
換句話說,一個 Student 可以有很多 Courses,而一個 Course 也能有多個 Student。 這個簡單的多對多關聯需要一個類似於如下結構的表:
id | student_id | course_id
現在,如果我們要存儲學生在課程上出席的天數及他們的最終級別,這張表將變成:
id | student_id | course_id | days_attended | grade
問題是,hasAndBelongsToMany 不支持這類情況,因爲 hasAandBelongsToMany 關聯被存儲時,先要刪除這個關聯。列中的額外數據會丟失,且放到新插入的數據中。
在 2.1 版更改.
你可以將 unique 設置爲 keepExisting 防止在保存過程丟失額外的數據。參閱 HABTM association arrays 中的 unique 鍵。
實現我們的要求的方法是使用一個 連接模型,或者也稱爲 hasMany 貫穿 關聯。 具體作法是模型與自身關聯。現在我們建立一個新的模型 CourseMembership。下面是此模型的定義。
// Student.php
class Student extends AppModel {
public $hasMany = array(
'CourseMembership'
);
}
// Course.php
class Course extends AppModel {
public $hasMany = array(
'CourseMembership'
);
}
// CourseMembership.php
class CourseMembership extends AppModel {
public $belongsTo = array(
'Student', 'Course'
);
}
CourseMembership 連接模型唯一標識了一個給定的學生額外參與的課程,存入擴展元信息中。
連接表非常有用,Cake 使其非常容易地與內置的 hasMany 和 belongsTo 關聯及 saveAll 特性一同使用。
在運行期間創建和銷燬關聯
有時候需要在運行時建立和銷燬模型關聯。比如以下幾種情況:
- 你想減少獲取的關聯數據的量,但是你的所有關聯都是循環的第一級。
- 你想要改變定義關聯的方法以便排序或者過濾關聯數據。
這種關聯的建立與銷燬由 CakePHP 模型 bindModel() 和 unbindModel() 方法完成。(還有一個非常有用的行爲叫 “Containable”,更多信息請參閱手冊中 內置行爲 一節)。 我們來設置幾個模型,看看 bindModel() 和 unbindModel() 如何工作。我們從兩個模型開始:
class Leader extends AppModel {
public $hasMany = array(
'Follower' => array(
'className' => 'Follower',
'order' => 'Follower.rank'
)
);
}
class Follower extends AppModel {
public $name = 'Follower';
}
現在,在 LeaderController 控制器中,我們能夠使用 Leader 模型的 find() 方法獲取一個 Leader 和它的 追隨者(followers)。就像你上面看到的那樣,Leader 模型的關聯關係數組定義了 “Leader hasMany Followers” 關係。爲了演示一下實際效果,我們使用 unbindModel() 刪除控制器動作中的關聯:
public function some_action() {
// 獲取 Leaders 及其相關的 Followers
$this->Leader->find('all');
// 刪除 hasMany...
$this->Leader->unbindModel(
array('hasMany' => array('Follower'))
);
// 現在使用 find 函數將只返回 Leaders,沒有 Followers
$this->Leader->find('all');
// NOTE: unbindModel 隻影響緊隨其後的 find 函數。再往後的 find 調用仍將使用預配置的關聯信息。
// 我們已經在 unbindModel() 之後使用了 find('all'),
// 所以此處將再次獲取 Leaders 及與其相關的 Followers ...
$this->Leader->find('all');
}
註解
使用 bindModel() 和 unbindModel() 來添加和刪除關聯,僅在緊隨其後的 find 操作中有效,除非第二個參數設置爲 false。如果第二個參數被設置爲 false,請求的剩餘位置仍將保持 bind 行爲。
以下是 unbindModel() 的基本用法模板:
$this->Model->unbindModel(
array('associationType' => array('associatedModelClassName'))
);
現在我們成功地在運行過程中刪除了一個關聯。 讓我們來添加一個。我們到今天仍沒有原則的領導需要一些關聯的原則。我們的 Principle 模型文件除了 public $name 聲明之外,什麼都沒有。 我們在運行中給我們的領導關聯一些 Principles(謹記它僅在緊隨其後的 find 操作中有效)。在 LeadersController 中的函數如下:
public function another_action() {
// 在 leader.php 文件中沒有 Leader hasMany Principles 關聯,所以這裏的 find 只獲取了 Leaders。
$this->Leader->find('all');
// 我們來用 bindModel() 爲 Leader 模型添加一個新的關聯:
$this->Leader->bindModel(
array('hasMany' => array(
'Principle' => array(
'className' => 'Principle'
)
)
)
);
// 現在我們已經正確的設置了關聯,我們可以使用單個的 find 函數來獲取帶有相關 principles 的 Leader:
$this->Leader->find('all');
}
bindModel() 的基本用法是封裝在以你嘗試建立的關聯類型命名的數組中的常規數組:
$this->Model->bindModel(
array('associationName' => array(
'associatedModelClassName' => array(
// normal association keys go here...
)
)
)
);
即使不需要通過綁定模型對模型文件中的關聯定義做任何排序,仍然需要爲使新關聯正常工作設置正確的排序鍵。
同一模型上的多個關係
有時一個模型有多個與其它模型的關聯。例如,你可能需要有一個擁有兩個 User 模型的 Message 模型。一個是要向其發送消息的用戶,一個是從其接收消息的用戶。 消息表有一個 user_id 列,還有一個 recipient_id。 你的消息模型看起來就像下面這樣:
class Message extends AppModel {
public $belongsTo = array(
'Sender' => array(
'className' => 'User',
'foreignKey' => 'user_id'
),
'Recipient' => array(
'className' => 'User',
'foreignKey' => 'recipient_id'
)
);
}
Recipient 是 User 模型的別名。來瞧瞧 User 模型是什麼樣的:
class User extends AppModel {
public $hasMany = array(
'MessageSent' => array(
'className' => 'Message',
'foreignKey' => 'user_id'
),
'MessageReceived' => array(
'className' => 'Message',
'foreignKey' => 'recipient_id'
)
);
}
它也可以建立如下的自關聯:
class Post extends AppModel {
public $belongsTo = array(
'Parent' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
public $hasMany = array(
'Children' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
}
獲取關聯記錄的嵌套數組:
如果表裏有 parent_id 使用不帶任何關聯設置的單個查詢的 find(‘threaded’) 來獲取記錄的嵌套數組。
連接表
在 SQL 中你可以使用 JOIN 子句綁定相關表。 這允許你運行跨越多個表的複雜查詢(例如:按給定的幾個 tag 搜索帖子)。
在 CakePHP 中一些關聯(belongsTo 和 hasOne)自動執行 join 以檢索數據,所以你能發出根據相關數據檢索模型的查詢。
但是這不適用於 hasMany 和 hasAndBelongsToMany 關聯。這些地方需要強制向循環中添加 join。你必須定義與要聯合的表的必要連接(join),使你的查詢獲得期望的結果。
註解
謹記,你需要將 recursion 設置爲 -1,以使其正常工作。例如: $this->Channel->recursive = -1;
在表間強制添加 join 時,你需要在調用 Model::find() 時使用 “modern” 語法,在 $options 數組中添加 ‘joins’ 鍵。例如:
$options['joins'] = array(
array('table' => 'channels',
'alias' => 'Channel',
'type' => 'LEFT',
'conditions' => array(
'Channel.id = Item.channel_id',
)
)
);
$Item->find('all', $options);
註解
注意 ‘join’ 數組不是一個鍵。
在上面的例子中,叫做 Item 的模型 left join 到 channels 表。你可以用模型名爲表起別名,以使檢索到的數組完全符合 CakePHP 的數據結構。
定義 join 所用的鍵如下:
- table: 要連接的表。
- alias: 表的別名。最好使用關聯模型名。
- type: 連接類型: inner, left 或者 right。
- conditions: 執行 join 的條件。
對於 joins 選項,你可以添加基於關係模型列的條件:
$options['joins'] = array(
array('table' => 'channels',
'alias' => 'Channel',
'type' => 'LEFT',
'conditions' => array(
'Channel.id = Item.channel_id',
)
)
);
$options['conditions'] = array(
'Channel.private' => 1
);
$privateItems = $Item->find('all', $options);
你可以在 hasAndBelongsToMany 中運行幾個需要的 joins:
假定一個 Book hasAndBelongsToMany Tag。這個關係使用一個 books_tags 表合爲連接表,你需要連接 books 表和 books_tags 表,並且帶着 tags 表::
$options['joins'] = array(
array('table' => 'books_tags',
'alias' => 'BooksTag',
'type' => 'inner',
'conditions' => array(
'Books.id = BooksTag.books_id'
)
),
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'BooksTag.tag_id = Tag.id'
)
)
);
$options['conditions'] = array(
'Tag.tag' => 'Novel'
);
$books = $Book->find('all', $options);
使用 joins 允許你以極爲靈活的方式處理 CakePHP 的關係並獲取數據,但是在很多情況下,你能使用其它工具達到同樣的目的,例如正確地定義關聯,運行時綁定模型或者使用 Containable 行爲。使用這種特性要很小心,因爲它在某些情況下可能會帶來模式不規範的 SQL 查詢。