Laravel

【Laravel】Eagerロードについて解説

こんにちは!ともです(@_tomo_engineer)!

今回はLaravelのEagerロードという機能について書きます。

僕はEagerロードが何を意味しているのか、何の為にあるのか良くわかりませんでしたが用途が分かりましたので書いていきます!

目次

  • Eagerロードとは
  • N+1問題とは
  • N+1問題の解決
  • Eagerロードの使い方

Eagerロードとは

Eagerとは「熱心」という意味ですが、

Eagerロードとは熱心にロードするってこと?

ってなりました。箇条書きすると以下です。

  • 先に取得したModelのコレクションに対して、事前にリレーションの取得
  • これによりN+1問題を解決する事ができる(クエリの発行を抑えれる)

日本語ドキュメントにはこのように書かれています。

プロパティとしてEloquentリレーションにアクセスする場合、そのリレーションデータは「遅延ロード」されます。つまり、そのリレーションデータが最初にアクセスされるまで、実際にはロードされません。しかし、Eloquentでは、親のモデルに対するクエリと同時にリレーションを「Eagerロード」可能です。EagerロードはN+1クエリ問題の解決策です。

全く意味が分からなかったんですが、つまりはこう言う事です。

User(投稿者) hasMany Post(投稿)の関係を例に考えます。

  • リレーションの情報は、リレーションデータにアクセスした時に取得する。

つまり

  • userモデルインスタンスはまだリレーションの情報を持っていない
  • user->postsとした時に初めてUserとPost間のリレーションデータを取得する

しかし、これだとN+1問題が発生してしまいます。

N+1問題

あるユーザ(User)の投稿(Post)をViewで表示するとします。

@foreach($users as $user)
    $user->posts;//ここでクエリを都度発行 
@endforeach

リレーション先にアクセスした時に、リレーションの情報を取得($user->postの時点でリレーション情報を取得)する訳ですから、投稿者(User)がN個の投稿(Post)をしている場合、このforeach分はN+1回ループします。

N+1回リレーションを取得するクエリが発行され、

select * from posts where id 1
select * from posts where id 2
select * from posts where id 3
select * from posts where id 4
(大量のクエリ発行)

N+1問題の解決

このN+1問題を解決する為にEagerロードは有効です。

この問題の原因はこれです。

リレーションプロパティにアクセスする度に、リレーション情報を取得する為、クエリを発行する

ですので、

事前にリレーション情報まで含め、親モデル(User)を取得しておけば良いのです。

select * from posts where id in (1,2,3・・・・)

この一回のクエリの事前発行(事前にリレーション情報まで取得)によりN+1問題を解決できます。

では、事前にリレーション情報まで含めて、親モデル(User)を取得する方法、Eagerロードについてご説明致します。

Eagerロードの使い方

1つのリレーション

// Eagerロードなし
$users = App\Post::all();
// Eagerロードあり
$users = App\Post::with('posts')->get();

with(‘posts’)により、Eagerロードは可能です。

複数のリレーション

// Eagerロードなし
$users = App\Post::all();
// Eagerロードあり(posts_tableとmails_table)
$users = App\Post::with(['posts', 'mails'])->get();

with()の引数に配列でリレーション名を与える事で複数のリレーション先のEagerロードが可能です。

事前にリレーション先を絞り込む

投稿(Post)のリレーション先をEagerロードしたいが、全ての投稿はいらない!

って場合もありますよね。あるユーザの投稿を事前に取得しておきたいが、2018年9月3日以降の投稿に関するリレーションだけ事前取得したい場合は次のように書きます。

$users = User::with(['posts' => function($query){
         $query->where('created_at', '>=', '2018-09-03');
}])->get();

これでEagerロードするリレーション先の情報を絞り込めます。

  • posts_table
id user_id content created_at
1 1 田中の投稿1 2018-09-02 09:00
2 1 田中の投稿2 2018-09-03 21:00
3 1 田中の投稿3 2018-09-04 19:00

このようなテーブルがあった場合、$users[0]->postsにアクセスしてみましょう。

Eagerロードするposts_tableのレコードは2018年9月3日以降ですので、のレコードが取得されます。9月2日に投稿されたものは除外されます。

Eagerロードする事を前提とする場合

Eagerロードする事が前提の場合、モデルのプロパティに次のように設定してみましょう。

  • Userモデル
protected $with = ['posts'];

これで、withメソッドを呼ばなくてもEagerロードされます。

次のようにしてSQLのログを取得し、確認しました。

  • Controller内でログの確認
       $users = User::all();
        // ログの取得
        DB::connection()->enableQueryLog();
        foreach($users as $user){
            $user->posts;
        }
        $queries = DB::getQueryLog();
        // ログの表示
        dd($queries);
  • protected $with=[‘posts’];無し
  • protected $with=[‘posts’];有り

Userモデル内の$withプロパティで設定した事により、Eagerロードされている事が分かります。その都度、SQLを発行していません。

まとめ

今回はEagerロードについてまとめました。

Eagerとは「熱心」という意味ですが、確かに事前にリレーションの情報まで取得するなんて勉強熱心だなと感じました。

大量のSQLを発行しない為にも、適切にEagerロードを使って行きたいです。

ご指摘等、お待ちしております。