多対多(HABTM)のアソシエーション

公式:
アソシエーション: モデル同士を繋ぐ - 2.x

アソシエーションの意味

データを取得する時に、関連するモデルの情報も持ってきてくれます。
コントローラでfind('all')したときに取得される結果が以下ですが、[AssociatedModelName]の値が関連モデルです。

Array
(
    [0] => Array
        (
            [ModelName] => Array
                (
                    [id] => 83
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

            [AssociatedModelName] => Array
                (
                    [id] => 1
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

        )
)

どのモデルにアソシエーションを設定するか

トランザクションテーブルのモデルに設定するとよい?もちろんマスターテーブルのモデルにも設定できるが、あまり必要な場面はなさそう?

テーブル構成を決める

例として、アンケートをフォームで受け付け結果をデータベースに格納することを考えます。アンケート内容は以下です。

※全て回答必須

項目名 内容
名前 氏名を教えてください。
年齢 年齢を教えてください。
趣味 次から選択してください(複数選択可)。
読書、音楽鑑賞、映画鑑賞、旅行、自転車、写真、その他

多対多のテーブル

まず趣味マスターとしてhobbiesテーブルを作ります。

id name
1 読書
2 音楽鑑賞
3 映画鑑賞
4 旅行
5 自転車
6 写真
7 その他

アンケート結果を格納するentriesテーブルを作ります。

id name age hobby
1 田中太郎 31 1, 5, 7
2 山田花子 20 1, 4
3 高橋次郎 28 3, 7
4 鈴木裕子 39 3

このとき、hobbiesとentriesは多対多(HABTM)といえます。
hobbiesの1つのレコードから見て、entriesにはたくさん対応するレコードがあります。
entriesの1つのレコードから見て、hobbiesにはたくさん対応するレコードがあります(id4鈴木さんのように一つしか対応しないものもあります)。

正規化

上のentriesテーブルは、1つのレコードに1つの値というデータベースの原則に沿っていません。多対多の時は、中間テーブルを作ってその原則に沿えるようにします(これを正規化といいます)。
中間テーブルentries_hobbiesを作り、以下のようにします。

entries

id name age
1 田中太郎 31
2 山田花子 20
3 高橋次郎 28
4 鈴木裕子 39

entries_hobbies

id entry_id hobby_id
1 1 1
2 1 5
3 1 7
4 2 1
5 2 4
6 3 3
7 3 7
8 4 3

これで1レコードに1値になりました。まとめたER図は以下です。
なおCakePHPは複合主キーに対応していない?ため、entries_hobbiesにもidという主キーを追加しています。

f:id:myhateno:20180525125216p:plain

CakePHPでアソシエーションを設定する

やっと本題です。以上のテーブル構成をアソシエーションとして、Entryモデルに設定します。

app/Model/Entry.php

<?php
class Entry extends AppModel {
    public $hasAndBelongsToMany = array(
        'Hobby' =>
            array(
                'joinTable' => 'entries_hobbies'
            )
    );
}

Entryにとって、$hasAndBelongsToMany(多対多)がHobby。
そのjoinTable(中間テーブル)がentries_hobbiesとCakePHPに教えています。
なおentries_hobbiesはCakePHPの中間テーブルの命名規則にそっているため、以下のようにjoinTableは教えなくてもOKです。

<?php
public $hasAndBelongsToMany = array(
    'Hobby'
);

これでEntryモデルをfind('all')したときに、hobbiesテーブルの情報もついてきます。