cakephp3のアソシエーションについて

最近使い方を知ったのでメモ

#アソシエーションとは
https://book.cakephp.org/3/ja/orm/associations.html
テーブル間に依存関係を定義し、`contain('model名')`のメソッドでまとめてデータを持ってくるもの。
裏側でjoinをしてる形になる。（多分）

#実装するメリット
毎回joinするようなテーブルはいちいち条件を書くのが面倒臭い。
join条件が決まってるのであればアソシエーションを組んだ方がすっきりするし、条件やカラムの変更があったときの手間が少ない

#種別
アソシエーションには4つの種別があり、それぞれでオプションとかが異なる。
違いはcookbookに言葉で書いてはあるが、正直個人的にはわかりづらい…

https://halfpower.work/2020/05/29/laravel%E3%81%AE%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-hasone%E3%80%81hasmany%E3%80%81belongsto%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/

の方がわかりやすく図式してくれてるのでおすすめ
（laravelなので微妙に違ってるかもだけど）

##具体例
１、authorsテーブル

|id  |name  |
|---|---|
|1 |tanaka  |
|2 |satou  |
|3 |ota  |

２、articlesテーブル

|id  |author_id  |url  | 
|---|---|---|
|1  |1  |hoge.html|
|2  |2  |fuga.html|
|3  |1  |piyo.html|

３、author_profileテーブル

|id  |author_id  |profile  | 
|---|---|---|
|1  |1  |すごい人|
|2  |2  |偉い人|
|3  |3  |エンジニアの人|

上記の例の場合、
###1=>3の参照
一対一対応でなければならないのでhasOne。この場合、１のidカラムと2のauthor_idカラムが対応している。
1のカラムにprofileも含めれば？と思うかもしれないけど私も思う。
ていうか現実でhasOneのアソシエーションがあったら大抵カラム追加しようってなる気がするんだけど実際どうなのでしょう…

###1=>2の参照
1のidと2のauthor_idを対応させてクエリを組むと、複数レコードが返ってくる。
かつ、１のidカラムはユニークなのでhasMany

###2=>1の参照
2のauthor_idカラムと1のidカラムを対応させてクエリを組むと単一のレコードが返ってくる。
かつ、2のauthor_idカラムはユニークではないのでbelongsTo

##belongsToManyについて
多対多のアソシエーションだが、こちらはちょっと複雑。
というのも、中間テーブルが必要になるため。
説明のために追加で下記のテーブルを考える

4、tagsテーブル

|id  |name  |
|---|---|
|1 |php  |
|2 |java  |
|3 |C#  |

5、author_responsible_tagsテーブル

|id  |author_id  |tag_id  |
|---|---|---|
|1 |1 |1  |
|2 |1 |2  |
|3 |2  |1  |
|4 |3  |3  |

この場合、authorは複数の担当tagを持つことがあり、かつtagは複数のauthorに共有されることがあるためアソシエーションとしてはtags <=> authorsの多対多になる。
が、その二つには関連するカラムが存在しないため、このままだとアソシエーションを組めない。
そこで関連づけるための中間テーブルを定義する必要がある。
（この場合はauthor_responsible_tags）
ということで、belongsToManyに関しては実質3テーブルが関わるアソシエーションとなる。

#実装方法
１、modelのinitializeでアソシエーションを宣言
２、findするときにcontain('model名')メソッドを呼び出す
これだけ。

##アソシエーションの宣言
上記具体例のテーブル名を使って記述する。
最低限の物だけ書くので、他のオプションはcookbook参照のこと。

###hasOne

```Table/Authors.php
$this->hasOne('AuthorProfiles', [
    'joinType' => 'LEFT OUTER',  //join方法
    'foreignKey' => 'author_id', //相手側のテーブルの参照カラム（model名_idなら省略できる。）
    'bindingKey' => 'id',        //自分側のテーブルの参照カラム（主キーなら省略できる。）
]);
```

###hasMany

```Table/Authors.php
$this->hasMany('Articles', [
    'joinType' => 'LEFT OUTER',  //join方法
    'foreignKey' => 'author_id', //相手側のテーブルの参照カラム（model名_idなら省略できる。）
    'bindingKey' => 'id',        //自分側のテーブルの参照カラム（主キーなら省略できる。）
]);
```

###belongsTo

```Table/Articles.php
$this->belongsTo('Authors', [
    'joinType' => 'LEFT OUTER',  //join方法
    'foreignKey' => 'author_id', //自分側のテーブルの参照カラム（相手側のmodel名_idなら省略できる。）
    'bindingKey' => 'id',        //相手側のテーブルの参照カラム（主キーなら省略できる。）
]);
```

###belongsToMany
中間テーブルがある分少し煩雑になる
これだけ実際に試してないので間違ってたらごめんなさい

```Table/Authors.php
$this->belongsToMany('Tags', [
    'joinType' => 'LEFT OUTER',               //join方法
    'joinTable' => 'author_responsible_tags', //中間テーブルの名前
    'foreignKey' => 'author_id',              //中間テーブル=>このmodelの参照カラム（自分のmodel名_idなら省略できる。）
    'bindingKey' => 'id',                     //foreignKeyに使われる自分側のテーブルの参照カラム（主キーなら省略できる。）
    'targetForeignKey' => 'tag_id',           //中間テーブル=>相手側のmodelの参照カラム（相手のmodel名_idなら省略できる。）
]);
```

##contain('model名')メソッドを呼び出す
例えばauthorカラムにアソシエーションしているarticlesも結果に含めたいのであれば、

```Table/Authors.php
$this->find()->contain('Articles')->all();
```

のような形で呼び出せば、結果のオブジェクトに`articles`プロパティ が増えており、そこにarticlesの結果が格納されているはず。
複数レコードがあるならちゃんとリストにもなる。
hasManyとbelongsToは片方が成り立てばもう一方も成り立つ関係だが、上記を呼び出すmodelにのみ定義すれば問題ない。

###注意点
上記の場合もwhereとかorderは当然できるが、joinする場合と同じく`Model名.カラム`で記述しないとエラーになるので注意。
例えば上記のクエリに加え、articleのid=1でwhereする場合は

```Table/Authors.php
$this->find()->contain('Articles')->where(['Articles.id' => 1])->all();
```

という書き方になる。
これはアソシエーションされているテーブルの全てのカラムでwhereするカラムを探すためで、たとえ片方にしかないカラムで検索する場合でも必要になる（はず）。