6.4. アソシエーション

6.4.1. はじめに

モデルが実現しているリレーショナルマッピングは、CakePHP の非常にパワフルな機能の一つです。CakePHP では、テーブル間のつながりをアソシエーションで扱います。アソシエーションは、論理的に関連しあったユニットをつなぐ“のり”です。

CakePHP には四種類のアソシエーションがあります:

  • hasOne

  • hasMany

  • belongsTo

  • hasAndBelongsToMany

モデル間のつながり(アソシエーション)が定義されると、 Cake は作業しているモデルに関係したモデルから自動的にデータを取り出します。例えば、Post (投稿)モデルが Author (著者)モデルに hasMany を使ってアソシエーションが設定されているなら、 コントローラで $this->Post->findAll() と呼び出すだけで、 Post レコード、また関連している Author レコードのデータを取り出します。

正しくアソシエーションを使うには、 CakePHP の命名規約に従うのが最善です。( 付録 "Cake 規約"を参照。) CakePHP の命名規約を使っている場合、 scaffolding でアプリケーションのデータを視覚化できます。 scaffolding は、モデル間のアソシエーションを検出して利用するからです。Cake の命名規約に従わない仕方でも モデルのアソシエーションをカスタマイズすることは可能です。しかし、そのTipsについては後ほど説明します。今は命名規約に従った方法を続けましょう。考えるべきなのは、外部キー(foreign keys)、モデル名、テーブル名に関する命名規約です。

次に挙げるのは、幾つかの要素の名前に関して、Cake が期待する事柄の復習です。:(命名に関する詳細な情報は、 付録 "Cake 規約" を参照)

  • 外部キー: [モデル名の単数形]_id。例えば、 "authors" テーブル内の外部キーが Post を指している場合、Author に 属しているものは、"post_id" という名前になります。

  • テーブル名:[オブジェクト名の複数形]。ブログの投稿(posts)とその作者(authors)の情報を保存したいので、テーブル名はそれぞれ、"posts" と "authors" になります。

  • モデル名:[CamelCased されたもの。テーブル名の単数形]。"posts" テーブルに対するモデルは、"Post"、そして "authors" テーブルには "Author"。

[注意] 注意

CakePHP の scaffolding は、アソシエーションが列の順番と同じだと期待します。ですから、もし Article が他の三つのモデル(Author、Editor、Publisher)に属している場合(belongsTo)、author_id、editor_id、publisher_id という三つのキーが必要になります。Scaffolding は、テーブル内のアソシエーションも同じ順番だと期待します。(つまり、まず Author 、2番目に Editor 、最後に Publisher)。

アソシエーションがどうやって働くかを説明するために、引き続きブログアプリケーションを例にしましょう。ブログ用にシンプルなユーザ管理システムを作るとします。そうなるともちろん Users の記録を取ることになりますが、さらにそれぞれのユーザが、つながりのある Profile (プロフィール)を一つ持つようにしたいと思います。(User hasOne Profile、ユーザはプロフィールを“一つ持つ”) Users はさらに、コメントを作ることができ、自分に関連付けることができます。(User hasMany Comments、ユーザはコメントを“複数持つ”)ひとたびユーザシステムが動くようになれば、Posts が関連するタグオブジェクトを、hasAndBelongsToMany の関係で持てるようにできます。( Post hasAndBelongsToMany Tags、 “投稿は複数のタグを持ち、また属する”)。

6.4.2. hasOne の定義と問い合わせ

このアソシエーションを設定するために、すでに User と Profile モデルが既に作成されているとします。この二つの間に hasOne のアソシエーションを定義するには、モデルに一つの配列を追加して Cake に関連の仕方を伝えます。次のような方法になります:

Example 6.4. /app/models/user.php hasOne

<?php
class User extends AppModel
{
    var $name = 'User';
    var $hasOne = array('Profile' =>
                        array('className' => 'Profile',
                              'conditions' => '',
                              'order' => '',
                              'dependent' => true,
                              'foreignKey' => 'user_id'
                        )
                  );
}
?>

$hasOne 配列は、User モデルと Profile モデルのアソシエーションを Cake が組み立てるのに用いられます。配列のそれぞれのキーによって、アソシエーションをさらに詳しく設定できます:

  • クラス名(必須):関連づけたいモデルのクラス名

    例では、 'Profile' モデルのクラス名を指定しています。

  • conditions: つながりを定義する SQL の条件の断片

    必要であれば、緑色のヘッダの付いたプロフィールだけを関連付けるように Cake に伝えることができます。その条件にしたければ、SQL 条件の断片として、キーをこのように指定できます:"Profile.header_color = 'green'"。

  • order: 関連するモデルのデータの並び順

    関連するモデルを特定の順番で取り出したい場合には、このキーに SQL の order の仕方で値を設定します。例えば、 "Profile.name ASC" などです。

  • dependent: true に設定すると、このモデルのデータの削除時に、関連しているモデル側のデータも削除されます。

    例えば、プロフィール "Cool Blue"が "Bob" に関連付けられていた場合、 "Bob" というユーザを削除すると、プロフィール"Cool Blue" も削除されます。

  • foreignKey: 関連しているモデルを指している外部キーの名前。

    Cake の命名規約に従っていないデータベースを使用している場合にここで指定できます。

User モデルで find() か findAll() を呼び出すと、関連している Profile モデルも同じように取得できます:(訳注:原文の User と Profile の書き間違い)

$user = $this->User->read(null, '25');
print_r($user);

//出力:

Array
(
    [User] => Array
        (
            [id] => 25
            [first_name] => ジョン
            [last_name] => アンダーソン
            [username] => psychic
            [password] => c4k3roxx
        )

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => アクアマリン
            [user_id] = 25
        )
)

6.4.3. belongsTo の定義と問い合わせ

User は Profile を見られるようになりました。今度は、Profile が User を見えるようにしましょう。Cake では、 belongsTo (~に属している)アソシエーションを使用することで可能になります。 Profile モデルの中で下記のようにします:

Example 6.5. /app/models/profile.php belongsTo

<?php
class Profile extends AppModel
{
    var $name = 'Profile';
    var $belongsTo = array('User' =>
                           array('className' => 'User',
                                 'conditions' => '',
                                 'order' => '',
                                 'foreignKey' => 'user_id'
                           )
                     );
}
?>

$belongsTo 配列は、Cake が User モデルと Profile モデルの間でのアソシエーションを作るのに使います。配列のそれぞれのキーによって、アソシエーションをさらに詳しく設定できます:

  • クラス名(必須):関連付けたいモデルのクラス名

    例では、 'User' モデルのクラス名を指定しています。

  • conditions: つながりを定義する SQL の条件の断片

    active な User だけを関連付けるよう Cake に伝える、というようなことができます。下記のようなキーを設定するとできます: "User.active = '1'" など。

  • order: 関連するモデルのデータの並び順

    関連するモデルを特定の順番で取り出したい場合には、このキーに SQL の order の仕方で値を設定します。例えば、 "User.last_name ASC" などです。

  • foreignKey: 関連しているモデルを指している外部キーの名前。

    Cake の命名規約に従っていないデータベースを使用している場合にここで指定できます。

Profile モデルで find() か findAll() を呼び出すと、関連している User モデルも同じように取得できます:

$profile = $this->Profile->read(null, '4');
print_r($profile);

//出力:

Array
(

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => アクアマリン
            [user_id] = 25
        )

    [User] => Array
        (
            [id] => 25
            [first_name] => ジョン
            [last_name] => アンダーソン
            [username] => psychic
            [password] => c4k3roxx
        )
)

6.4.4. hasMany の定義と問い合わせ

User モデルと Profile モデルが関連し、ふさわしく動くようになったので、 User レコードが Comment レコードと関連するようにしてみましょう。これは、 User モデルを次のようにすることで可能です:

Example 6.6. /app/models/user.php hasMany

<?php
class User extends AppModel
{
    var $name = 'User';
    var $hasMany = array('Comment' =>
                         array('className' => 'Comment',
                               'conditions' => 'Comment.moderated = 1',
                               'order' => 'Comment.created DESC',
                               'limit' => '5',
                               'foreignKey' => 'user_id',
                               'dependent' => true,
                               'exclusive' => false,
                               'finderQuery' => ''
                         )
                  );

    // さきほど定義した hasOne のリレーション...
    var $hasOne = array('Profile' =>
                        array('className' => 'Profile',
                              'conditions' => '',
                              'order' => '',
                              'dependent' => true,
                              'foreignKey' => 'user_id'
                        )
                  );
}
?>

$hasMany 配列は、 Cake が User モデルと Comment モデルのアソシエーションを作る時に使用されます。配列のそれぞれのキーによって、アソシエーションをさらに詳しく設定できます:

  • クラス名(必須):関連付けたいモデルのクラス名

    例では、 'Comment' モデルのクラス名を指定しています。

  • conditions: つながりを定義する SQL の条件の断片

    調整された Comment だけを関連付けるように、と Cake に指示することができます。このキーの値を "Comment.moderated = 1" などのようにして、設定できるでしょう。

  • order: 関連するモデルのデータの並び順

    関連するモデルを特定の順番で取り出したい場合には、このキーに SQL の order の仕方で値を設定します。例えば、 "Comment.created DESC" などです。

  • limit: Cake が取り出す関連モデルのデータの最大数。

    この例では、ユーザのコメントを *すべて* ではなく、5つだけ取り出します。

  • foreignKey: 関連しているモデルを指している外部キーの名前。

    Cake の命名規約に従っていないデータベースを使用している場合にここで指定できます。

  • dependent: true に設定すると、このモデルのデータの削除時に、関連しているモデル側のデータも削除されます。

    例えば、プロフィール "Cool Blue" が "Bob" に関連付けられていた場合、 "Bob" というユーザを削除すると、プロフィール "Cool Blue" も削除されます。

  • exclusive: true に設定すると、関連しているすべてのオブジェクトが一つの SQL ステートメントで削除されます。 beforeDelete コールバックは実行されません。

    動作が速いので、シンプルなアソシエーションで活用できます。

  • finderQuery: アソシエーションを取り出すために、完全な SQL ステートメントを指定します。

    複数のテーブルに依存する複雑なアソシエーションの場合に活用できます。もし Cake の自動アソシエーションが使えない場合には、これでカスタマイズできます。

User モデルで find() か findAll() を呼び出すと、関連している Comment モデルも同じように取得できます:

$user = $this->User->read(null, '25');
print_r($user);

//出力:

Array
(
    [User] => Array
        (
            [id] => 25
            [first_name] => ジョン
            [last_name] => アンダーソン
            [username] => psychic
            [password] => c4k3roxx
        )

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => アクアマリン
            [user_id] = 25
        )

    [Comment] => Array
        (
            [0] => Array
                (
                    [id] => 247
                    [user_id] => 25
                    [body] => hasMany アソシエーションは便利だ。
                )

            [1] => Array
                (
                    [id] => 256
                    [user_id] => 25
                    [body] => hasMany アソシエーションは便利だ。
                )

            [2] => Array
                (
                    [id] => 269
                    [user_id] => 25
                    [body] => hasMany アソシエーションは便利だ。
                )

            [3] => Array
                (
                    [id] => 285
                    [user_id] => 25
                    [body] => hasMany アソシエーションは便利だ。
                )

            [4] => Array
                (
                    [id] => 286
                    [user_id] => 25
                    [body] => hasMany アソシエーションは便利だ。
                )

        )
)
[注意] 注意

プロセスを文書で説明することはしませんが、 "Comment belongsTo User" アソシエーションも定義して、お互いのモデルから相手が見えるようにするのは良い考えです。 お互いのモデルからアソシエーションを定義していないと、 scaffolding を使おうとした時、ごっちゃになることがあります。

6.4.5. hasAndBelongsToMany の定義と問い合わせ

シンプルなアソシエーションについてはこれでマスターできました。次に、最後のアソシエーションに移りましょう。 hasAndBelongsToMany (または HABTM)です。これは理解するのが簡単ではありませんが、非常に便利なものの一つです。 HABTM アソシエーションは、二つのテーブルがあり、その二つを join テーブルでつなげている時に役立ちます。 join テーブルが他と関連している個々の列の情報を持ちます。

hasMany と hasAndBelongsToMany の違いは、hasMany の場合、関連しているモデルのデータを共有できない、ということにあります。もし User hasMany Comments (ユーザが複数のコメントを持っている)ならば、コメントに関連しているのは、ユーザ *だけ* です。 HABTM では、関連したモデルデータを共有することができます。これは次の場合などに力を発揮します。Post モデルを Tag モデルに関連づける場合などです。 Tag は Post に belongs to の(属する)関係ですが、“使い切ってしまう” ことは望ましくありません。他の Post からも関連付けたいと思います。

そうするためには、このアソシエーションのためにテーブル同士を正しく設定する必要があります。もちろん、Tag モデルのために "tags" テーブル、そして Post モデルのために "posts" テーブルが必要です。そして、このアソシエーションのために、さらに join テーブルの作成も必要です。 HABTM join テーブルの命名規約は、 [複数形モデル名1]_[複数形モデル名2] というもので、モデル名はアルファベット順に並べます:

Example 6.7. HABTM Join テーブル:サンプルモデルと join テーブルの名前

  • Posts と Tags: posts_tags

  • Monkeys と IceCubes: ice_cubes_monkeys

  • Categories と Articles: articles_categories


HABTM join テーブルは、接続するモデルに関して、最少で二つの外部キーから構成されています。この例では、 "post_id" と "tag_id" だけが必要です。

Posts HABTM Tags の SQL ダンプの例です:

--
-- `posts` テーブルの構造
--

CREATE TABLE `posts` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `user_id` int(10) default NULL,
  `title` varchar(50) default NULL,
  `body` text,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  `status` tinyint(1) NOT NULL default '0',
  PRIMARY KEY (`id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- `posts_tags` テーブルの構造
--

CREATE TABLE `posts_tags` (
  `post_id` int(10) unsigned NOT NULL default '0',
  `tag_id` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY (`post_id`,`tag_id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- `tags` テーブルの構造
--

CREATE TABLE `tags` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `tag` varchar(100) default NULL,
  PRIMARY KEY (`id`)
) TYPE=MyISAM;

テーブルの設定ができたら、Post モデルのアソシエーションを設定します:

Example 6.8. /app/models/post.php hasAndBelongsToMany

<?php
class Post extends AppModel
{
    var $name = 'Post';
    var $hasAndBelongsToMany = array('Tag' =>
                               array('className' => 'Tag',
                                     'joinTable' => 'posts_tags',
                                     'foreignKey' => 'post_id',
                                     'associationForeignKey'=> 'tag_id',
                                     'conditions' => '',
                                     'order' => '',
                                     'limit' => '',
                                     'unique' => true,
                                     'finderQuery' => '',
                                     'deleteQuery'=> '',
                               )
                               );
}
?>

$hasAndBelongsToMany 配列は、 Cake が Post モデルと Tag モデルの間でのアソシエーションを作るのに使います。配列のそれぞれのキーによって、アソシエーションをさらに詳しく設定できます:

  • クラス名(必須):関連付けたいモデルのクラス名

    例では、 'Tag' モデルのクラス名を指定しています。

  • joinTable: Cake の命名規約に従っていないデータベースの場合はここで設定します。 [複数形モデル1]_[複数形モデル2] の辞書的な順番になっていない場合には、テーブル名をここで指定します。

  • foreignKey: 現在のモデルを指す join table 内の外部キー名。

    Cake の命名規約に従っていないデータベースを使っている場合にはここで指定します。

  • associationForeignKey: 関連しているモデルを指す外部キーの名前。

  • conditions: つながりを定義する SQL の条件の断片

    調整された Comment だけを関連付けるように、と Cake に指示することができます。このキーの値を "Comment.moderated = 1"などのようにして、設定できるでしょう。

  • order: 関連するモデルのデータの並び順

    関連するモデルを特定の順番で取り出したい場合には、このキーに SQL の order の仕方で値を設定します。例えば、"Comment.created DESC" などです。

  • limit: Cake が取り出す関連モデルのデータの最大数。

    この例では、ユーザのコメントを *すべて* ではなく、5つだけ取り出します。

  • unique: true に設定すると、アクセスとクエリの際、関連したオブジェクトが重複している場合には無視されます。

    基本的に、つながりが別個になっている場合には、 true に設定します。そうすると、 "Awesomeness" というタグは、Post の "Cake のモデルアソシエーション" に1度だけ割り当てられ、結果配列の中には、1度しか現れません。

  • finderQuery: アソシエーションを取り出すために、完全な SQL ステートメントを指定します。

    複数のテーブルに依存する複雑なアソシエーションの場合に活用できます。もし Cake の自動アソシエーションが使えない場合には、これでカスタマイズできます。

  • deleteQuery: HABTM モデル間のアソシエーションのデータを取り除く 完全な SQL ステートメントです。

    Cake の削除の仕方が望ましくなかったり、設定がカスタマイズされている場合などに、削除の動作方法を、自分のクエリをここに設定することで変更できます。

Post モデルで find() か findAll() を呼び出すと、関連している Tag モデルも見ることができます:

$post = $this->Post->read(null, '2');
print_r($post);

//出力:

Array
(
    [Post] => Array
        (
            [id] => 2
            [user_id] => 25
            [title] => Cake モデルアソシエーション
            [body] => 時間の短縮になり、簡単、パワフル
            [created] => 2006-04-15 09:33:24
            [modified] => 2006-04-15 09:33:24
            [status] => 1
        )

    [Tag] => Array
        (
            [0] => Array
                (
                    [id] => 247
                    [tag] => CakePHP
                )

            [1] => Array
                (
                    [id] => 256
                    [tag] => Powerful Software
                )
        )
)

6.4.6. hasAndBelongsToMany の保存

hasOne, belongsTo, hasMany で関連付けられているモデルを保存するのは非常に簡単です。関連するモデルの ID と、外部キーのフィールドを設定するだけです。そのあとモデルの save() メソッドを呼べば、つながっているすべてのものが正しく組み立てられます。

hasAndBelongsToMany はもう少しトリッキーですが、できるだけシンプルにやってみましょう。例を進める際、 Tags から Posts に関連している、あるフォームを作成する必要があります。 posts を作成するフォームを作成して、既にある Tags の一覧と関連づけてみましょう。

実際には、新しいタグを作成して、その場で記事と関連づけられるようにしたいと思うかもしれません。しかし今回は、シンプルにするために、どうやって関連づけて、取り出すかというところだけを示します。

Cake で自分のモデルに保存したい場合、(HtmlHelper を使っているなら) タグ名は、 'モデル/フィールド_名前' のようになります。投稿を作成するフォームをまず作成しましょう:

Example 6.9. posts 作成のための /app/views/posts/add.thtml フォーム

<h1>新規投稿の書き込み</h1>
   <table>
	 <tr>
		<td>タイトル:</td>
		<td><?php echo $html->input('Post/title')?></td>
	 </tr>
	 <tr>
		<td>本文:<td>
		<td><?php echo $html->textarea('Post/body')?></td>
	 </tr>
	 <tr>
    	 <td colspan="2">
        	<?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
			<?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
			<?php echo $html->submit('投稿を保存しますか')?>
    	 </td>
	</tr>
</table>
			 

このフォームは Post レコードを作成できるようになりました。今度は、一つかそれ以上のタグを Post につなげられるようにしましょう:

Example 6.10. /app/views/posts/add.thtml (タグアソシエーションのコードを追加)

<h1>新規投稿の書き込み</h1>
<table>
	<tr>
		<td>タイトル:</td>
		<td><?php echo $html->input('Post/title')?></td>
	</tr>
	<tr>
		<td>本文:</td>
		<td><?php echo $html->textarea('Post/body')?></td>
	</tr>
	<tr>
		<td>関連タグ:</td>
		<td><?php echo $html->selectTag('Tag/Tag', $tags, null, array('multiple' => 'multiple')) ?>
		</td>
	</tr>
	<tr>
		<td colspan="2">
			<?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
			<?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
			<?php echo $html->submit('投稿を保存しますか')?>
		</td>
	</tr>
</table>
		 

新規の Post とそれに関連した Tags を保存するためにコントローラで $this->Post->save() を呼ぶには、フィールド名が フォームの "Tag/Tag" の中になければなりません。 submit するデータは単一の ID か、リンクしたレコードの ID の配列でなければなりません。ここでは multiple セレクトを使っているので、Tag/Tag に送信されるデータは ID の配列になります。

ここにある $tags 変数は、利用できるタグの ID をキー、multi-select 要素のタグの表示名を値、とする配列です。