CakePHP3のcontainとは?EagerLoadingを行う便利なメソッド

PHP

CakePHP3のcontainメソッドは、EagerLoadingを自動で行ってくれる便利なメソッドです。

containメソッドの使い方

CakePHP3のcontainメソッドは、引数に指定したアソシエーション先テーブルのデータをEagerLoadingしてくれます。
引数はテーブル名のarrayです。
例えば、Postsコントローラで、Postsテーブルとアソシエーションしているtagsテーブルをcontainしたい時は、以下のように書きます。


$query = $this->Posts->find('all')->contain(['tags']);

もちろん、複数のテーブルをcontainできます。


$query = $this->Posts->find('all')->contain(['tags', 'comments']);

containメソッドがやってくれること

containメソッドを使うとEagerLoadingをやってくれると書きましたが、そもそもなんやそれ?って感じだったので調べてみました。
EagerLoadingというのは、簡単にいうと事前に必要なデータをLoadしておくことで、これが後述するN+1問題の解決に役立ちます。

アソシエーション先のデータをとってこようとすると、N+1問題と言われるものが発生してしまいます。

N+1問題

SQLクエリがデータの量N+1回走ってしまい、取得するデータ量Nが増えるたびに発行クエリも増えてしまいパフォーマンスが低下する問題のことです。

例えば、
下の画像のようなuser_idで関連づけられているUsersテーブルとPostsテーブルがあるとします。
UsersテーブルからみたPostsテーブルとのアソシエーションは1対多(hasMany)で、PostsテーブルからみたUsersテーブルとのアソシエーションは多対1(belongsTo)です。

この画像の「投稿一覧」のような画面を作りたいとすると、Postsテーブルのアソシエーション先であるUsersテーブルのカラム(Users.name)も必要になります。

この時、PostsControllerでPostsテーブル(PostsTableクラスのインスタンス)のみを読み込んでビューにsetし、index.ctpでforeachを回してPostsの一覧を表示するとします。

PostsController.php

class PostsController extends AppController
{
    public function index()
    {
        $this->loadComponent('Paginator');
        $posts = $this->Paginator->paginate($this->Posts->find());
        $this->set(compact('posts'));
    }
}

すると、ループ1回につき1文、(1投稿につき1文)Users.nameを取得するためのSQLクエリが発行されてしまいます。


//以下のsqlが投稿数N個分だけ発行される
select name from Users where id = (そのループ番におけるPostsのuser_id)

投稿数をNとすると、
まず全投稿を取ってくるためのクエリ


select * from posts

が1回と、
Users.nameを取得するためのクエリ


//以下のsqlが投稿数N個分だけ発行される
select name from Users where id = (そのループ番におけるPostsのuser_id(foreign key))

がN回走ることになるので、合計でN+1個のSQLクエリが発行されてしまいます。
これをN+1問題と言います。

データが少量の時はそこまで問題はないのですが、データが大量にある時はNの大きさもかなりのものになるので、パフォーマンスの大幅な低下を招いてしまいます。

EagerLoading

これを防ぐのがEagerLoadingで、先のコントローラーにおいてPostsテーブルの読み込み時に、アソシエーション先のUsersテーブルのデータもまとめて事前にLoadしておきます。
そうすることで既に必要なすべてのデータが用意された状態でビューへのsetが行われるため、Templateにおいてループのたびに必要なデータを取得するためのSQLクエリが走るようなことがなくなります。

CakePHP3では、モデルのloadはデフォルトでlazyloading(後から必要になったタイミングでloadする)なので、containメソッドを使ってEagerLoadingを明示的に行わないといけません。

先程のPostsとUsersの例では、PostsControllerで


class PostsController extends AppController
{
    public function index()
    {
        $this->loadComponent('Paginator');
        $posts = $this->Paginator->paginate($this->Posts->find()->contain(['Users']);
        $this->set(compact('posts'));
    }
}

のようにcontainメソッドを使い引数にUsersテーブル名を指定してあげるだけでOKです。CakePHP3の力でEagerLoadingを自動的に行ってくれます。

このクエリビルダーによって裏ではどのようなSQLが走っているのかというと、このようなbelongsToアソシエーションで、結合タイプをLeftJoinにしている場合は


select posts.*, users.* from posts left join users on users.id = posts.user_id

みたいなSQLです。(これはだいぶざっくりなもので、実際にはもっといい感じにSQLクエリを作ってくれます)

逆に1対多、つまりhasManyアソシエーションの時(UsersControllerでPostsテーブルのデータもEagerLoadしたい時)は、


select * from users
select * from posts

ざっくりとはこの2文です。

containメソッドはこれらのようなSQLを作ってくれてデータを取得し、さらに2つの情報を紐付けた多次元配列を作成し、返してくれます。

hasOneアソシエーションの場合はご想像の通り簡単なので書きませんが、とにかく実際に発行されるSQLは、アソシエーションの種類や結合タイプによって異なります

いちいちこれらのSQLを書き分けるのは面倒臭い上、その紐付けを行うのも大変なので、有名なフレームワークにはだいたいEagerLoadingのためのメソッドがあります。
その1つがCakePHP3におけるcontainメソッドなのです。

まとめ

CakePHP3のcontainメソッドは、N+1問題を解消するためのEagerLoadingを自動で行ってくれるメソッドでした。
TableにhasManyなどのアソシエーション設定をしておけば、Controllerではcontain(‘アソシエーション先Tabel名’)のようなメソッドを書くだけで、勝手にアソシエーションに合わせたEagerLoadingを行ってくれるので、大変便利ですね。

コメント

タイトルとURLをコピーしました