「クエリが大量に発行されているせいでタイムアウトしちゃってる」
といったことを避けるために重要な"Eager loading"。
Eloquent リレーションで Eager loading するにはなんとなく with
メソドを使うということぐらいしか知らなかったので、
ドキュメントの Eloquent: Relationships > Eager loading
の部分を読んでみました。
参考
公式ドキュメント
https://laravel.com/docs/5.6/eloquent-relationships#eager-loading
Eloquent のリレーション
EloquentのリレーションはEloquentモデルにメソドで定義をする。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get all of the posts for the user.
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}
呼び出すときは
$user->posts()->where('active', 1)->get();
という感じ。
リレーションメソドではクエリビルダが使える。
リレーションメソド と 動的プロパティ
リレーションクエリに制約を追加する必要がない場合、プロパティのように ->
でリレーションにアクセスすることができ、リレーションメソドに対してこれは「動的プロパティ」という。
$user = App\User::find(1);
//->posts() じゃなく、 ->posts になってるのが動的プロパティ
foreach ($user->posts as $post) {
//
}
リレーションデータの Eager Loading
- 全部のデータをとってきて、
- (動的)プロパティで毎回リレーションデータをとってくる。
というようにしてしまうと、レコード数分だけクエリが発行されてしまう。
//lazy loading
$books = App\Book::all(); //クエリを1回発行したあと、
foreach ($books as $book) {
echo $book->author->name; //booksの冊数分(N回)だけ発行される。
}
よく言う「N+1」問題というやつ。
クエリを1回+N回発行してしまうと、大規模なデータになるほど、データをとるのにものすごく時間がかかってしまってタイムアウトになってしまうこともある。
これを避けるため、先にリレーションデータも取得しておくのが "Eager loading" であり、Eloquentでは with
メソドを使う。
//Eager loading
$books = App\Book::with('author')->get(); //クエリが発行されるのは2回だけ
foreach ($books as $book) {
echo $book->author->name;
}
こうやって with
メソドを使えば、"Eager loading" でリレーションデータを取得することができ、クエリの発行はたった2回で済む。
リレーションメソドを使ってしまうと Eager loading にしてても意味がないので注意
$books = App\Book::with('author')->find(1);;
$author = $user->author()->where('id', 1)->get();
リレーションメソドの場合、先ほどの例のように ...->where('active', 1)->...
とクエリビルダを使って制約が追加することができる。
ただし、毎回クエリを発行してデータを取りに行ってしまうことになるため、先にEager Loadingでリレーションデータを取ってきていても、そのあとにリレーションメソドを使ってしまうと意味がない。
一方の動的プロパティはキャッシュが聞くため、EloquentでEager Loadingをする場合は動的プロパティの方を使う。
参考
https://qiita.com/katsunory/items/d4757b8d9ac099a5291e
Eager Loading のいろんなやり方
複数のリレーションをとることもできる
with
の引数に配列を使って、複数のリレーションデータをとることもできる。
$books = App\Book::with(['author', 'publisher'])->get();
ネストされたテーブルのリレーションだってとることができる
「リレーションされたテーブルのリレーション」というネストされたテーブルのデータもEager loadingで取得することができる。
これは知らんかった。今度から使おう。
$books = App\Book::with('author.contacts')->get();
特定のカラムだけをとることもできる
$users = App\Book::with('author:id,name')->get();
この場合、 id
は必ずとる必要があることに注意。
Eager loadingするリレーションデータを限定したいとき
「titleにfirstを含むリレーションデータだけをEager Loadingしたい」というときは with
の中でクロージャを使えば可能。
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
注意点
posts
のクエリでデータがとれないユーザーは $users
に入ってこないのかと思っていたが、その場合は posts
がnullになるだけで $users
には入ってくる。
( with
はリレーションデータをとってくるメソド)
「titleにfirstを含むリレーションデータを持たないユーザーはいらない」と、リレーションデータをとってきたいのではなく リレーションデータによる絞り込み をしたい場合は whereHas
メソドを使う。
$users = App\User::whereHas('posts', function($query) {
$query->where('title', 'like', '%first%');
})->get();
「先に親データとっちゃったけど、追加で子のデータもEager Loadingしたい」というのもできる
「すでに親のデータだけとっちゃったけど、リレーションデータをEager loadingでとりたい」というときには load
メソドを使う。
(ドキュメントでは"Lazy Eager loading"と表記されている)
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
load
メソドも with
と同様、追加でクエリを発行することが可能。
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
Eager loadし忘れたやつだけとる、という loadMissing
メソドもある
public function format(Book $book)
{
$book->loadMissing('author');
return [
'name' => $book->name,
'author' => $book->author->name
];
}
「all
メソドで実装しちゃった...」という経験は何度かあるので、この load
loadMissing
メソドは覚えておくと便利そう。
まとめ
- Eloquentのリレーションはリレーションメソドを使う
- リレーションメソドではクエリビルダ、チェーンメソドが使える。
- 制約を追加する必要がないのであれば動的プロパティとしてアクセスすることができる
- EloquentでEager Loadingする場合は
with
メソドを使って、データは動的プロパティでとってくる - EloquentにはEager Loadingする手段がいくつも用意されている
- あとからEager Loadとかもできる