I'm attending CakeFest 2010!

3.7.6.5 hasAndBelongsToMany (HABTM)

The original text for this section has changed since it was translated. Please help resolve this difference. You can:

More information about translations

さて、この時点で、すでに CakePHP におけるモデルの関連付けの専門家になっていることでしょう。すでに3つの関連に精通し、オブジェクトの関連付けの大半を学んできました。

それでは最後の関連である hasAndBelongsToMany もしくは HABTM に取り組みましょう。この関連が使用されるのは、2つのモデルがあり、それらがさまざまな方法で繰り返し何度も連携する必要がある場合です。

hasMany と HABTM の間の主な違いは、HABTM 内のモデル間の結びつきが排他的ではないということです。たとえば、HABTM を使用して Recipe モデルが Tag モデルと連携するとします。おばあちゃんのニョッキ(訳注:イタリアの伝統料理)レシピに "Italian" というタグを割り当てても、タグを「使い切る」ことにはなりません。蜂蜜でテカテカの BBQ スパゲッティにも、"Italian" とタグ付けできます。

hasMany 関連のオブジェクトの間の結びつきは排他的です。User が Comment と hasMany である場合、コメントは特定のユーザにのみ結び付けられます。あるユーザに結びついたコメントは、もう他のユーザに結びつけることはできません。

先に進めましょう。HABTM 関連を扱うには、追加のテーブルをデータベースにセットアップする必要があります。この新しい追加のテーブルの名前は、両方のモデルの名前が含まれており、それらをアルファベット順に並べてアンダースコア(「_」)で繋げたものにします。テーブルは少なくも2つのフィールドを含み、それぞれの外部キー(integer にすべき)が各モデルの主キーである必要があります。問題を避けるために、これら2つのフィールドを複合主キーにしないでください。アプリケーションにおいてそうする必要がある場合は、ユニークなインデックスを定義します。このテーブルに何か情報を追加する場合は、他のモデルと同じように簡単に扱えるよう、主キーのフィールド(規約上は「id」という名前のフィールド)を追加するとよいでしょう。

HABTM 両方のモデル名を含んだテーブルを追加する必要があります

関係 スキーマ
Recipe HABTM Tag id, recipes_tags.recipe_id, recipes_tags.tag_id
Cake HABTM Fan id, cakes_fans.cake_id, cakes_fans.fan_id
Foo HABTM Bar id, 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'              => 'recipes_tags',
                 'with'                   => '',
                'foreignKey'             => 'recipe_id',
                'associationForeignKey'  => 'tag_id',
                'unique'                 => true,
                'conditions'             => '',
                'fields'                 => '',
                'order'                  => '',
                'limit'                  => '',
                'offset'                 => '',
                'finderQuery'            => '',
                'deleteQuery'            => '',
                'insertQuery'            => ''
            )
    );
}
?>
  1. <?php
  2. class Recipe extends AppModel {
  3. var $name = 'Recipe';
  4. var $hasAndBelongsToMany = array(
  5. 'Tag' =>
  6. array(
  7. 'className' => 'Tag',
  8. 'joinTable' => 'recipes_tags',
  9. 'with' => '',
  10. 'foreignKey' => 'recipe_id',
  11. 'associationForeignKey' => 'tag_id',
  12. 'unique' => true,
  13. 'conditions' => '',
  14. 'fields' => '',
  15. 'order' => '',
  16. 'limit' => '',
  17. 'offset' => '',
  18. 'finderQuery' => '',
  19. 'deleteQuery' => '',
  20. 'insertQuery' => ''
  21. )
  22. );
  23. }
  24. ?>

HABTM 関連の配列で指定可能なキーは次の通りです:

  • className: 現在のモデルに関連したモデルのクラス名。‘Recipe HABTM Tag’ という関係を定義する場合、className キーは‘Tag’ になります。
  • joinTable: 関連で使用する結合テーブルの名前(もし HBTM の結合テーブル名が規約に従っていない場合)
  • with: 結合テーブルのモデル名の定義。デフォルトでは CakePHP は自動的にモデルを生成します。上述の例だと、RecipesTag が呼び出されます。デフォルトの名前を上書きするために、このキーを使います。結合テーブルのモデルは、あらゆる「通常の」モデルのように、結合テーブルへアクセスするために使用することができます。
  • foreignKey: 現在のモデルにある外部キーの名前。複数の HABTM 関係を定義する必要がある場合に特に便利です。このキーのデフォルト値は、現在のモデル名のアンダースコア区切りの単数形で、末尾に‘_id’をつけたものです。
  • associationForeignKey: もう一方のモデルにある外部キーの名前。複数の HABTM 関係を定義する必要がある場合に特に便利です。このキーのデフォルト値は、もう一方のモデル名のアンダースコア区切りの単数形で、末尾に‘_id’をつけたものです。
  • unique: もし true(デフォルト)なら、Cake は更新を行う際、外部キーのテーブルに新たなレコードを挿入する前に既存の関連レコードを削除します。したがって、更新を行う際には、既存の関連するレコードをもう一度渡す必要があります。
  • conditions: 関連モデルのレコードを限定するための SQL。SQL 内でモデル名を使用することを習慣にしておくようにしておきましょう:“status = 1.” よりも、“Comment.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] => 124
                    [name] => Dessert
                )
           [2] => Array
                (
                    [id] => 125
                    [name] => Heart Disease
                )
        )
)

Tag モデルを使用する際に Recipe データを取得したい場合は、Tag モデルに HABTM の関連を定義することを覚えておいてください。

HABTM 関連に基づいた独自の find クエリを実行することもできます。次の例をみてください:

上記の例と同じ構造(Recipe HABTM Tag)を仮定し、'Dessert' タグをもつすべての Recipe を取得したいとします。これを達成できる一つの方法(ただし悪い方法)は、アソシエーションそのものに検索する条件を適用することです:

$this->Recipe->bindModel(array(
	'hasAndBelongsToMany' => array(
		'Tag' => array('conditions'=>array('Tag.name'=>'Dessert'))
)));
$this->Recipe->find('all');
  1. $this->Recipe->bindModel(array(
  2. 'hasAndBelongsToMany' => array(
  3. 'Tag' => array('conditions'=>array('Tag.name'=>'Dessert'))
  4. )));
  5. $this->Recipe->find('all');
//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] => 124
                        [name] => Dessert
                    )
            )
    )
    1 => Array
        {
        [Recipe] => Array
            (
                [id] => 2745
                [name] => Crab Cakes
                [created] => 2008-05-01 10:31:01
                [user_id] => 2349
            )
        [Tag] => Array
            (
            }
        }
}

この例では "Dessert" タグがついた全てのレシピしか返せないことに注意してください。これをきちんと達成するには、いくつかの方法があります。一つの方法は、Recipe ではなく Tag モデルを検索し、関連付いた Recipe も全て取得する方法です。

$this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
  1. $this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));

与えられた ID を検索するために、CakePHP が提供する結合テーブルのモデルを使うことも出来ます。

$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
$this->Recipe->find('all', array(
		'fields' => array('Recipe.*'),
		'conditions'=>array('RecipesTag.tag_id'=>124) // id of Dessert
));
  1. $this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
  2. $this->Recipe->find('all', array(
  3. 'fields' => array('Recipe.*'),
  4. 'conditions'=>array('RecipesTag.tag_id'=>124) // id of Dessert
  5. ));

フィルタリングを行うために必要な数の結合を生成するために、風変わりな関連付けを作成することもできます。例を見てください:

$this->Recipe->bindModel(array(
	'hasOne' => array(
		'RecipesTag',
		'FilterTag' => array(
			'className' => 'Tag',
			'foreignKey' => false,
			'conditions' => array('FilterTag.id = RecipesTag.tag_id')
))));
$this->Recipe->find('all', array(
		'fields' => array('Recipe.*'),
		'conditions'=>array('FilterTag.name'=>'Dessert')
));
  1. $this->Recipe->bindModel(array(
  2. 'hasOne' => array(
  3. 'RecipesTag',
  4. 'FilterTag' => array(
  5. 'className' => 'Tag',
  6. 'foreignKey' => false,
  7. 'conditions' => array('FilterTag.id = RecipesTag.tag_id')
  8. ))));
  9. $this->Recipe->find('all', array(
  10. 'fields' => array('Recipe.*'),
  11. 'conditions'=>array('FilterTag.name'=>'Dessert')
  12. ));

両方の例は、次のデータを返します:

//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] => Breakfast
                )
           [1] => Array
                (
                    [id] => 124
                    [name] => Dessert
                )
           [2] => Array
                (
                    [id] => 125
                    [name] => Heart Disease
                )
        )
}

同じようなバインディングトリックで、 HABTM モデルのページ付けを簡単にすることができます。一点だけ注意が必要です。ページ付けで2つのクエリ(レコードの数を数えるものと、実際のデータを取得するもの)が必要な場合、必ず bindModel();false パラメータをセットしてください。こうすることで、デフォルトのビヘイビアのように、単一ではなく複数のクエリにまたがってモデルの関連が維持されます。詳細は API に関する文書を参照してください。

アソシエーションをその場で取り扱うことについての詳しい情報は、その場でアソシエーションを生成、廃棄の章を参照してください。

その時々の目的に応じて、これらのテクニックを組み合わせたり適用してください。