N+1問題について
with
やjoin
を使う背景にはこのN+1問題があるので、まずはN+1問題について簡単に説明します。
理解しているよという人は読み飛ばしてください!!
Laravelでこのようなリレーションが設定されていたとします。
/**
* purchasesテーブルへのリレーション(1対多)
*
* @return HasMany
*/
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
以下のようにしてデータを取得することができます。
// customersテーブルから全件取得
$customers = Customer::query()->get();
foreach ($customers as $customer) {
// purchasesテーブルのデータへアクセスできる
$customer->purchases;
}
上記コードでN+1問題が発生しています。
発行されたクエリを確認してみます。
ご覧の通り一番上のクエリでcustomersテーブルから全件取得しているクエリが1件あります。
そしてその下に続いているクエリはcustomersテーブルから取れた件数分クエリが発行されています。foreachで回されるたびにクエリを発行しています。
つまり最初のクエリで取れた件数がN件だった場合、最初の全件取得のクエリの1回 + N回のクエリが発行されるということです。
N+1より1+Nの方が分かりやすいかもしれません。
仮に数万人のcustomerがいたとしたら数万件のクエリが発行されることになりDBへの負荷がとても高い状態になりますし、スピードもかなり遅くなってしまいます。
これを解消するためにLaravelにはwith
とjoin
があります。
withを使う場合
まずはwith
を使って解消してみます。
// customersテーブルから全件取得
$customers = Customer::query()
->with('purchases')
->get();
foreach ($customers as $customer) {
// purchasesテーブルのデータへアクセスできる
$customer->purchases;
}
なんとクエリの件数が2件になりました。
見て分かるようにwhere inを使って複数指定して一括で取得しています。
このようなクエリにすれば、いくらcustomersテーブルの件数が増えてもクエリの件数は変わることはありません。
joinを使う場合
join
を使う場合も見てみましょう。
// customersテーブルから全件取得
$customers = Customer::query()
->join('purchases', 'customer.id', '=', 'purchases.customer_id')
->get();
こちらはクエリ1件で取得ができています。
どちらを使ってもN+1問題は解消することができます。
ではどちらを使う方がいいのでしょうか?
2つの詳しい違いについて説明します!
withとjoinの違い
まずそれぞれのやり方で取得したデータを見ていきます。
withの場合
$customers = Customer::query()
->with('purchases')
->get();
$customers->count();
$customers[0];
こちらの取得結果が以下です。
customersの件数が1001件です。
そして、$customers[0]
の型はCustomerです。なのでattributesを見るとcustomersテーブルのカラムの情報が取得できています。with
で取得したpurchases
はrelationsのところで取得できているのが分かると思います。
これがwith
で取得した場合のデータの情報です。
joinの場合
$customers = Customer::query()
->join('purchases', 'customer.id', '=', 'purchases.customer_id')
->get();
$customers->count();
$customers[0];
joinの場合は、件数が30000件になっています。これはcustomersの件数ではなく紐付くpurchasesのデータの件数になっています。
さらにattributesの中の最後の2行を見るとcustomersテーブルには存在しないカラムが追加されています。これはpurchasesテーブルのカラムです。
型はCustomerなのにpurchasesのデータも取れてしまっています。これはアクティブレコードの利点を失ってしまいます。
型はCustomerですが実体はCustomerでもPurchaseでもないものになっています。
ORMはテーブルとモデルクラスが1対1の関係で紐づいているのが前提ですがその関係が崩れてしまって不可解な挙動になり、バグが起こる原因になってしまいます。
なので結論、withを使うようにしましょう!
もしjoinを使う場合は
// Eloquentビルダをクエリビルダに変換
$row = Customer::query()
->toBase()
->join('purchases', 'customer.id', '=', 'purchases.customer_id')
->get();
// もしくは
// モデルではなくテーブルをクエリする
$row = DB::table('purchases')
->join('purchases', 'customer.id', '=', 'purchases.customer_id')
->get();
注意点としては結果セットを変形するクエリをEloquentから使うのを避けたり、モデルなのかレコードセットなのか変数名などで明確に区別してあげる必要があります。
また、Eloquentとクエリビルダを混ぜて使うのはバグが紛れ込む原因になるので基本的にはどちらかに統一した方がいいと思います。
個人的にはLaravelを使ってるならEloquentを使った方がいいと思ってます。
Eloquentを使ってこそLaravelを使う意味があるんじゃないのかなと思っているからです。
Laravelの開発者のTaylor Otwellさんも開発時に一番大変だったのがEloquentと言っていたそうなのでちゃんと使いこなしてあげたいです。。
以上です!
最後まで読んでいただきありがとうございました!!
参考記事