CakePHP3でデータベースを引くときに、他のテーブルを取り込めるcontain
という機能があります。単にデータを取ってくるだけでなく、更に細かい制御も行えます。
contain
の必要性
CakePHP3ではモデルクラスもTableとEntityに分かれたことで、1レコードがEntityオブジェクトとなったのですが、その結果発生しうるのがN+1問題です。Entityの中から関連づいたテーブルを参照しようとすると、それぞれのEntityごとにクエリが飛ぶため、クエリの数が膨れ上がってしまうのです。
これを防ぐために、データベースから関連するレコードを一気に引いてしまう、という手法があって、以下のどちらかで実装されています。
- 関連する別テーブルとJOINすることで、まとめてデータを引く
- 関連付けするキーを使って別のテーブルを引いて、あとから合成する
CakePHP3で、このような「まとめて引く」ための操作を行うのがcontain
です。
contain
の制御の必要性
モデルを内部的に使う場合にしても、不要な列まで取ってくる必要はありません。そしてさらに、Crudプラグインを使う場合、引いてからデータを加工するというのが面倒になるので、クエリ段階で調節できればそれが最適解となります。
具体的な書き方
基本編
まず、contain
メソッドはクエリオブジェクトにあるものなので、$someTable->find()->contain()
のように呼び出します。contain
の引数には配列を与えますが、普通の配列と連想配列が混ざったような、慣れるまでは不思議な形で与えます。
-
'OtherItems'
のようなキーだけ→そのテーブルをcontain
する -
'AnotherItems' => ['YetOtherItem']
のような「キー => 配列」の指定→キーのテーブルへ、さらに配列の中身をcontain
する(このネストは何度でも繰り返せます)
列を絞る
そして、contain
に与える配列に'テーブル名' => function($q){}
のようなクロージャを与えると、$q
にはcontain
先のテーブルについてのクエリオブジェクトが来ますので、ここで各種の制御ができます。
たとえば、「Users
で必要な列はid
とname
だけ」というような場合、以下のように書けばそうとれます。
$this->Items->find()
->contain(['Users' => function($q){
return $q->select(['id', 'name']);
}]);
なお、列を絞るときには注意点が2つあります。
- リレーションに必要な列は必ず入れてください(そうしないとリレーションを成立させられなくなります)。
- Virtual Propertyを仕掛けてある場合など、Entity側でコードを書いている場合、「列が減った状態」でもエラーにならないか確認の必要があります。
また、このようにコールバックを取ったcontain
に別なリレーションをcontain
したい場合は、$q->contain()
というようにチェーンさせてください。
Virtual Propertyの制御
「Crudに使うときには不要」ということでVirtual Propertyを止めたいこともあるかもしれませんが、これを行うときはEntityオブジェクト自体を書き換える必要が出てきます。ということで、formatResults()
という関数の出番です。ここでは、Product
にあるVirtual Propertyをを全部止める、という例で考えてみます。
$this->Transactions->find()
->contain(['Products'=> function($q){
return $q->formatResults(function($products){
return $products->map(function($product){
$product->virtualProperties([]);
return $product;
});
});
}]);
ご覧のとおり、これを行うだけで3重のクロージャが必要となってしまいます。ただ、「全部止める」だけであればクロージャを使いまわせますので、何かのヘルパーで生成して流用、というのが現実的ではないかと思います。