Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
23
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

@jkr_2255

CakePHP3のcontainを操る

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で必要な列はidnameだけ」というような場合、以下のように書けばそうとれます。

$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重のクロージャが必要となってしまいます。ただ、「全部止める」だけであればクロージャを使いまわせますので、何かのヘルパーで生成して流用、というのが現実的ではないかと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
23
Help us understand the problem. What are the problem?