3.7.6 関連: モデルを結びつける
CakePHP の最も強力な機能の1つは、モデルによって提供されるリレーショナルマッピングを結びつける能力です。CakePHP では、モデル間の結びつきは関連を通して処理されます。
アプリケーション内で異なるオブジェクト間の関係を定義することは自然なことです。たとえば: recipe データベース内で、recipe は複数の reviews を持ち、reviews は1つの author を持ちます。また、author は複数の recipe を持ちます。このような関係を定義すると、直感的かつ強力な方法でデータにアクセスすることができます。
この章の目的は、設計の仕方や定義の仕方やCakePHP でモデル間の関連の使用方法を示すことです。
データは様々なところからやってきますが、ウェブアプリケーションで最も一般的なストレージはリレーショナルデータベースです。この章で扱う大半はリレーショナルデータベースを想定しています。
3.7.6.1 関連の形式
CakePHP で使用する関連の形式は: hasOne, hasMany, belongsTo, hasAndBelongsToMany (HABTM) です。
| 関係 | 関連の形式 | 例 |
|---|---|---|
| 1対1 | hasOne | user は1つの profile をもつ |
| 1対多 | hasMany | システムの User は複数の recipe をもつことができる |
| 1対多 | belongsTo | recipe は user に属する |
| 多対多 | hasAndBelongsToMany | Recipe は複数の tag をもち、かつ属する |
関連は、定義しようとしている関連の後に名づけたクラス変数によって定義されます。クラス変数は文字列にすることもできますが、関連の設定を定義するために多次元配列にすることもできます。
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = 'Profile';
var $hasMany = array(
'Recipe' => array(
'className' => 'Recipe',
'conditions' => 'Recipe.approved = 1',
'order' => 'Recipe.created DESC'
)
);
}
?>
<?phpclass User extends AppModel {var $name = 'User';var $hasOne = 'Profile';var $hasMany = array('Recipe' => array('className' => 'Recipe','conditions' => 'Recipe.approved = 1','order' => 'Recipe.created DESC'));}?>
上記の例では、'Recipe' という単語の最初のインスタンスが 'エイリアス' になります。これは関係で使用する ID となり、これを指定するだけで選択できます。通常、クラス名と同じ名前を選択しますが、エイリアスは単独のモデル内、かつ belongsTo/hasMany または belongTo/hasOne 関連においてユニークでなければなりません。モデルのエイリアスにユニークでない名前を選択すると、予期しない振る舞いをします。
3.7.6.2 hasOne
では、User モデルを作成しましょう。このモデルは、Profile モデルと hasOne の関係があります。
まず、データベースのテーブルは正確にキーが設定されている必要があります。hasOne の関係が動作するには、あるテーブルは外部キーを持つ必要があり、そのキーはもう一方のレコードを指し示します。この場合、profile テーブルは、user_id というフィールドを持っています。基本的な形式は:
| 関係 | スキーマ |
|---|---|
| Apple hasOne Banana | bananas.apple_id |
| User hasOne Profile | profiles.user_id |
| Doctor hasOne Mentor | mentors.doctor_id |
User モデルファイルを /app/models/user.php に保存します。‘User hasOne Profile’の関係を定義するために、モデルクラスに $hasOne プロパティを追加します。/app/models/profile.php 内に Profile モデルを指定することを忘れないでください。そうしないと関連は動作しません。
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = 'Profile';
}
?>
<?phpclass User extends AppModel {var $name = 'User';var $hasOne = 'Profile';}?>
モデルファイル内にこの関係を記述するには2つの方法があります。もっとも簡単な方法は、上記で指定したように関連モデルのクラス名を含む文字列を $hasOne プロパティにセットすることです。
より細かい制御が必要な場合、配列を使用して関連を定義することができます。たとえば、日付の降順で関連する行をソートしたい、あるいは1つだけのレコードを含むように関連を限定したいというような場合です。
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = array(
'Profile' => array(
'className' => 'Profile',
'conditions' => 'Profile.published = 1',
'dependent' => true
)
);
}
?>
<?phpclass User extends AppModel {var $name = 'User';var $hasOne = array('Profile' => array('className' => 'Profile','conditions' => 'Profile.published = 1','dependent' => true));}?>
hasOne 関連の配列で指定可能なキーは:
- className: 現在のモデルに関連したモデルのクラス名。‘User hasOne Profile’ という関係を定義する場合、className キーは‘Profile’ になります。
- foreignKey: もう一方のモデルにある外部キーの名前。複数の hasOne 関係を定義する必要がある場合に特に便利です。このキーのデフォルト値は、現在のモデル名のアンダースコア区切りの単数形で、末尾に‘_id’をつけたものです。上記の例では、'user_id' となります。
- conditions: 関連モデルのレコードを限定するために使用する SQL。SQL 内でモデル名を使用するのはよい習慣です:“Profile.approved = 1” は常に“approved = 1.” よりもよい記述です。
- fields: 関連モデルのデータが取得された際に取り出すフィールドのリストです。デフォルトではすべてのフィールドを返します。
- 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
)
)
3.7.6.3 belongsTo
ここでは、User モデルから Profile のデータにアクセスします。User のデータに関連したデータにアクセスするために Profile モデルに belongsTo 関連を定義します。belongsTo 関連は、自然に hasOne や hasMany 関連の対になります: 他の方向からデータをみることができます。
データベースのテーブルに belongsTo 関係のためのキーを作成するには、次のような規則になります:
| 関係 | スキーマ |
|---|---|
| Banana belongsTo Apple | bananas.apple_id |
| Profile belongsTo User | profiles.user_id |
| Mentor belongsTo Doctor | mentors.doctor_id |
モデル(テーブル)が外部キーを持つ場合、そのモデルは他のモデル(テーブル)に属します。
次のような構文を使用して、/app/models/profile.php 内で Profile モデルに belongsTo 関連を定義することができます:
<?php
class Profile extends AppModel {
var $name = 'Profile';
var $belongsTo = 'User';
}
?>
<?phpclass Profile extends AppModel {var $name = 'Profile';var $belongsTo = 'User';}?>
配列を使用してより詳細な関係を定義することもできます。
<?php
class Profile extends AppModel {
var $name = 'Profile';
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
?>
<?phpclass Profile extends AppModel {var $name = 'Profile';var $belongsTo = array('User' => array('className' => 'User','foreignKey' => 'user_id'));}?>
belongsTo 関連の配列で有効なキーは以下のようになります:
- className: 現在のモデルに関連したモデルのクラス名。‘Profile belongsTo User’ という関係を定義する場合、className キーは‘User’になります。
- foreignKey: 現在のモデルにある外部キー名。複数の belongsTo 関係を定義する必要がある場合に、これは特に便利です。このキーのデフォルト値は、他のモデル名のアンダースコアで区切られた単数形で、末尾に‘_id’が付きます。
- conditions: 関連モデルのレコードを限定するために使用する SQL。SQL にモデル名を使用するのは良い習慣となります: “User.active = 1” は常に“active = 1”よりも推奨されます。
- fields: 関連モデルのデータを取得した際に取り出すフィールドのリスト。デフォルトではすべてのフィールドを返します。
- counterCache: (bool) true にセットすると、save() または delete() が呼び出されるたびに、関連モデルは自動的に外部テーブルの“[singular_model_name]_count”というフィールドをインクリメントまたはデクリメントします。カウンタフィールドの値は関連する行の番号を表します。
この関連が定義されると、Profile モデルの find 操作は、存在する場合は関連する User レコードを取得するでしょう:
//Sample results from a $this->Profile->find() call.
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
)
)
3.7.6.4 hasMany
次のステップ: “User hasMany Comment” という関連を定義します。 hasMany 関連を定義すると、User レコードを取得する際に、ユーザのコメントも取得できます。
データベースのテーブルに hasMany 関係のためのキーを作成するには、次のような規則になります:
| 関係 | スキーマ |
|---|---|
| User hasMany Comment | Comment.user_id |
| Cake hasMany Virtue | Virtue.cake_id |
| Product hasMany Option | Option.product_id |
次のような構文を使用して、/app/models/user.php 内で User モデルに hasMany 関連を定義することができます。
<?php
class User extends AppModel {
var $name = 'User';
var $hasMany = 'Comment';
}
?>
<?phpclass User extends AppModel {var $name = 'User';var $hasMany = 'Comment';}?>
配列を使用してより詳細な関係を定義することもできます。
<?php
class User extends AppModel {
var $name = 'User';
var $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id',
'conditions' => 'Comment.status = 1',
'order' => 'Comment.created DESC',
'limit' => '5',
'dependent'=> true
)
);
}
?>
<?phpclass User extends AppModel {var $name = 'User';var $hasMany = array('Comment' => array('className' => 'Comment','foreignKey' => 'user_id','conditions' => 'Comment.status = 1','order' => 'Comment.created DESC','limit' => '5','dependent'=> true));}?>
hasMany 関連の配列で有効なキーは以下のようになります:
- className: 現在のモデルに関連したモデルのクラス名。‘User hasMany Comment’ という関係を定義する場合、className キーは‘Comment’になります。
- foreignKey: 他のモデルにある外部キー名。複数の hasMany 関係を定義する必要がある場合に、これは特に便利です。このキーのデフォルト値は、他のモデル名のアンダースコアで区切られた単数形で、末尾に‘_id’が付きます。
- conditions: 関連モデルのレコードを限定するために使用する SQL。SQL にモデル名を使用するのは良い習慣となります: “Comment.status = 1” は常に“status = 1”よりも推奨されます。
- fields: 関連モデルのデータを取得した際に取り出すフィールドのリスト。デフォルトではすべてのフィールドを返します。
- order: 返される関連する行のソート順を定義する SQL。
- limit: 返して欲しい関連する行の最大数。
- offset: 取り出し関連付ける前に(与えられた現在の条件と順番で)スキップする関連する行の数。
-
dependent: dependent キーが true にセットされると、再帰的なモデルの削除が可能になります。この例の場合、Comment レコードは、関連する User レコードが削除されたときに、同時に削除されます。
Model->delete()メソッドのの第2パラメータは、再帰的な削除をするためには true をセットしなければなりません。 - finderQuery: 関連モデルのレコードを取得するために CakePHP が使用できる完全な SQL。これは独自の結果が必要な場合に使用されます。
この関連が定義されると、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] => 123
[user_id] => 121
[title] => More on Gwoo
[body] => But what of the ‘Nut?
[created] => 2006-05-01 10:41:01
)
)
)
1つ覚えておいてほしいのは、両方の方向からデータを取得するためには、Comment が User と belongsTo 関連である必要があります。この章で述べてきたことは、User から Comment データを取得することができるということです。Comment モデル内で User と belongsTo 関連を追加すると、Comment モデルから User データを取得できます - 接続が完全であれば、どちらかのモデルからみても情報を流すことができます。
3.7.6.5 hasAndBelongsToMany (HABTM)
この時点で、すでに CakePHP モデルの関連のプロになっていることでしょう。すでに3つの関連に精通してきて、オブジェクトの関係の大半を学んできました。
最後の関係に取り組みましょう: hasAndBelongsToMany もしくは HABTM です。この関連が使用されるのは、2つのモデルがあり、それらがさまざまな方法で繰り返し何度も連携する必要がある場合です。
hasMany と HABTM の間の主な違いは、HABTM 内のモデル間の結びつきが排他的ではないということです。たとえば、HABTM を使用して Recipe モデルが Tag モデルと連携するとします。grandma の Gnocci レシピに “Italian” というタグを割り当てても、タグを“使い切る”ことにはなりません。蜂蜜でテカテカの BBQ スパゲッティにも、“Italian”とタグ付けできます。
hasMany 関連のオブジェクトの間の結びつきは排他的です。User が Comment と hasMany である場合、コメントは特定のユーザにのみ結び付けられます。それはもう他のユーザに結びつけることはできません。
先に進めましょう。特別なテーブルをデータベースに作成し、HABTM 関連を扱う必要があります。この新しい追加のテーブルの名前は、両方のモデルの名前が含まれている必要があり、アルファベット順である必要があります。テーブルの内容は少なくも2つのフィールドがあり、各外部キー(integer であるべきです)がそれぞれモデルの主キーである必要があります。
| 関係 | スキーマ |
|---|---|
| Recipe HABTM Tag | recipes_tags.recipe_id, recipes_tags.tag_id |
| Cake HABTM Fan | cakes_fans.cake_id, cakes_fans.fan_id |
| Foo HABTM Bar | bars_foos.foo_id, bars_foos.bar_id |
テーブル名は規約によりアルファベット順です。
新しいテーブルが作成されると、モデルファイルに HABTM 関連を定義できます。ここではすぐに配列構文にいきましょう。:
<?php
class Recipe extends AppModel {
var $name = 'Recipe';
var $hasAndBelongsToMany = array(
'Tag' =>
array('className' => 'Tag',
'joinTable' => 'posts_tags',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'tag_id',
'conditions' => '',
'order' => '',
'limit' => '',
'uniq' => true,
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
?>
<?phpclass Recipe extends AppModel {var $name = 'Recipe';var $hasAndBelongsToMany = array('Tag' =>array('className' => 'Tag','joinTable' => 'posts_tags','foreignKey' => 'recipe_id','associationForeignKey' => 'tag_id','conditions' => '','order' => '','limit' => '','uniq' => true,'finderQuery' => '','deleteQuery' => '','insertQuery' => ''));}?>
hasMany 関連の配列で有効なキーは以下のようになります:
- className: 現在のモデルに関連したモデルのクラス名。‘User hasMany Comment’ という関係を定義する場合、className キーは‘Comment’になります。
- joinTable: この関連(現在のテーブルが HABTM の結合テーブルの命名規約に準じていない場合)で使用される結合テーブルの名前。
- foreignKey: 他のモデルにある外部キー名。複数の HABTM 関係を定義する必要がある場合に、これは特に便利です。このキーのデフォルト値は、他のモデル名のアンダースコアで区切られた単数形で、末尾に‘_id’が付きます。
- associationForeignKey: 現在のモデルにある外部キー名。複数の HABTM 関係を定義する必要がある場合に、これは特に便利です。このキーのデフォルト値は、他のモデル名のアンダースコアで区切られた単数形で、末尾に‘_id’が付きます。
- conditions: 関連モデルのレコードを限定するために使用する SQL。SQL にモデル名を使用するのは良い習慣となります: “Comment.status = 1” は常に“status = 1”よりも推奨されます。
- fields: 関連モデルのデータを取得した際に取り出すフィールドのリスト。デフォルトではすべてのフィールドを返します。
- order: 返される関連する行のソート順を定義する 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
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Breakfast
)
[1] => Array
(
[id] => 123
[name] => Dessert
)
[2] => Array
(
[id] => 123
[name] => Heart Disease
)
)
)
Tag モデルを使用する際に、Recipe データを取得したい場合は、Tag モデルに HABTM 関連を定義することを覚えておいてください。
HABTM 関連に基づいた独自の find クエリを実行することもできます。次の例をみてください:
上記例(Recipe HABTM Tag)と同じ構造と仮定します。"Dessert" タグをもつすべての Recipe を取得したいとします。簡単な(だが悪い)方法は、Recipe モデルで複雑な条件を使用することです:
$this->Recipe->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
$this->Recipe->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
// 返されるたデータ
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Dessert
)
)
)
1 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Crab Cakes
[created] => 2008-05-01 10:31:01
[user_id] => 2349
)
[Tag] => Array
(
}
}
}
この例では、すべてのレシピを返していますが、"Dessert" タグについてのみであることに注意してください。目的を達成するには、HABTM 関連を使用して単に同じクエリーを実行することです。
$this->Recipe->RecipesTag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
$this->Recipe->RecipesTag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
//Data Returned
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Dessert
)
)
)
}
3.7.6.6 その場でアソシエーションを生成、廃棄
その場でモデルのアソシエーションを生成したり廃棄したりする必要がときどきあります。例えば、次のような理由があります:
- 取ってくる関連データを減らしたいが、すべてのアソシエーションが第一レベルのrecursionで設定されている。
- 関連データをソートしたりフィルタリングしたりするために、アソシエーションの設定を変えたい。
このアソシエーションの生成や廃棄は、CakePHP のbindModel()やunbindModel()などのモデルメソッドを使って実現できます。("Containable" という非常に便利なビヘイビアもあります。詳細は組み込みのビヘイビアについてのマニュアルを参照してください)では、モデルを設定してみて、bindModel() や unbindModel() がどのように動作するかを見てみましょう。2つのモデルで始めます:
<?php
class Leader extends AppModel {
var $name = 'Leader';
var $hasMany = array(
'Follower' => array(
'className' => 'Follower',
'order' => 'Follower.rank'
)
);
}
?>
<?php
class Follower extends AppModel {
var $name = 'Follower';
}
?>
<?phpclass Leader extends AppModel {var $name = 'Leader';var $hasMany = array('Follower' => array('className' => 'Follower','order' => 'Follower.rank'));}?><?phpclass Follower extends AppModel {var $name = 'Follower';}?>
LeadersController では、Leader やそれに関連する Follower を取得するために、Leader モデル内の find() メソッドを使用できます。上記に示したように、Leader モデル内の 関連配列には "Leader hasMany Followers" という関係を定義しています。デモとして、コントローラのアクション内でその関連を廃棄するために unbindModel() を使用してみましょう。
function someAction() {
// これは Leader を取得して、Follower も取得します
$this->Leader->findAll();
// hasMany を削除しましょう
$this->Leader->unbindModel(
array('hasMany' => array('Follower'))
);
// ここで find 関数を使用すると、
// Leaders を返しますが、Follower は返しません。
$this->Leader->findAll();
// 注意: unbindModel はすぐ次の find 関数にのみ影響します。
// その次の find 呼び出しは設定済みの関連情報を使用して
// 呼び出されます。
// unbindModel() の後にすでに findAll() を使用してしまったので、
// ここでは Leader とともに関連する Follower も取得されます。
$this->Leader->findAll();
}
function someAction() {// これは Leader を取得して、Follower も取得します$this->Leader->findAll();// hasMany を削除しましょう$this->Leader->unbindModel(array('hasMany' => array('Follower')));// ここで find 関数を使用すると、// Leaders を返しますが、Follower は返しません。$this->Leader->findAll();// 注意: unbindModel はすぐ次の find 関数にのみ影響します。// その次の find 呼び出しは設定済みの関連情報を使用して// 呼び出されます。// unbindModel() の後にすでに findAll() を使用してしまったので、// ここでは Leader とともに関連する Follower も取得されます。$this->Leader->findAll();}
もう1点。第2引数に false をセットしない限り、bindModel() や unbindModel() を使用した関連の削除や追加は、 次の モデル操作のみに作用します。第2引数が false にセットされると、bind は指定されたままの状態になります。次に unbindModel() の基本的な使用方法のパターンを示します:
$this->Model->unbindModel(
array('associationType' => array('associatedModelClassName'))
);
$this->Model->unbindModel(array('associationType' => array('associatedModelClassName')));
その場でアソシエーションを削除できたので、今度は追加してみましょう。まだ何も設定されていない Leader には Principle("指導方針") を関連づけないといけません。Principle モデルのモデルファイルは、変数 $name 以外の設定は書き込まれていません。その場で Leader に Principleモデルを関連付けてみましょう。(しかし次の find 操作にのみ影響することを忘れないでください)この関数は LeadersController 内にあります:
function anotherAction() {
// leader.php モデルファイル内には
// Leader hasMany Principle がないのでここでは
// Leader のみ取得します。
$this->Leader->findAll();
// bindModel() を使用して Leader モデルに新しい関連を
// 追加しましょう:
$this->Leader->bindModel(
array('hasMany' => array(
'Principle' => array(
'className' => 'Principle'
)
)
)
);
// 正しく関連付けされたので
// 1回の find 関数で Leader を取得すると
// 関連する Principle も取得されます:
$this->Leader->findAll();
}
function anotherAction() {// leader.php モデルファイル内には// Leader hasMany Principle がないのでここでは// Leader のみ取得します。$this->Leader->findAll();// bindModel() を使用して Leader モデルに新しい関連を// 追加しましょう:$this->Leader->bindModel(array('hasMany' => array('Principle' => array('className' => 'Principle'))));// 正しく関連付けされたので// 1回の find 関数で Leader を取得すると// 関連する Principle も取得されます:$this->Leader->findAll();}
bindModel() の基本的な使い方は、通常の関連配列と同じで、キーは作成しようとしている関連の種類の後に記述します:
$this->Model->bindModel(
array('associationName' => array(
'associatedModelClassName' => array(
// 通常の関連のキーをここに記述します
)
)
)
);
$this->Model->bindModel(array('associationName' => array('associatedModelClassName' => array(// 通常の関連のキーをここに記述します))));
新しく結合されたモデルは、モデルファイル内に関連の定義は必要ありませんが、適切に新しい関連が動作するためには正しくキーを設定する必要があります。
