LoginSignup
46
19

More than 1 year has passed since last update.

Laravelのwithとjoinの違いについて

Posted at

N+1問題について

withjoinを使う背景にはこのN+1問題があるので、まずはN+1問題について簡単に説明します。
理解しているよという人は読み飛ばしてください!!

Laravelでこのようなリレーションが設定されていたとします。

Customer.php
/**
 * 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問題が発生しています。
発行されたクエリを確認してみます。

スクリーンショット 2023-04-13 23.31.31.png

ご覧の通り一番上のクエリでcustomersテーブルから全件取得しているクエリが1件あります。
そしてその下に続いているクエリはcustomersテーブルから取れた件数分クエリが発行されています。foreachで回されるたびにクエリを発行しています。
つまり最初のクエリで取れた件数がN件だった場合、最初の全件取得のクエリの1回 + N回のクエリが発行されるということです。
N+1より1+Nの方が分かりやすいかもしれません。

仮に数万人のcustomerがいたとしたら数万件のクエリが発行されることになりDBへの負荷がとても高い状態になりますし、スピードもかなり遅くなってしまいます。

これを解消するためにLaravelにはwithjoinがあります。

withを使う場合

まずはwithを使って解消してみます。

// customersテーブルから全件取得
$customers = Customer::query()
    ->with('purchases')
    ->get();

foreach ($customers as $customer) {
    // purchasesテーブルのデータへアクセスできる
    $customer->purchases;
}

この状態でクエリを確認してみましょう!
スクリーンショット 2023-04-13 23.48.14.png

なんとクエリの件数が2件になりました。
見て分かるようにwhere inを使って複数指定して一括で取得しています。
このようなクエリにすれば、いくらcustomersテーブルの件数が増えてもクエリの件数は変わることはありません。

joinを使う場合

joinを使う場合も見てみましょう。

// customersテーブルから全件取得
$customers = Customer::query()
    ->join('purchases', 'customer.id', '=', 'purchases.customer_id')
    ->get();

この時のクエリがこちらです。
スクリーンショット 2023-04-14 0.16.36.png

こちらはクエリ1件で取得ができています。

どちらを使ってもN+1問題は解消することができます。
ではどちらを使う方がいいのでしょうか?

2つの詳しい違いについて説明します!

withとjoinの違い

まずそれぞれのやり方で取得したデータを見ていきます。

withの場合

$customers = Customer::query()
    ->with('purchases')
    ->get();

$customers->count();
$customers[0];

こちらの取得結果が以下です。
スクリーンショット 2023-04-15 16.03.09.png
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];

取得結果はこちらです。
スクリーンショット 2023-04-15 16.35.44.png

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と言っていたそうなのでちゃんと使いこなしてあげたいです。。

以上です!
最後まで読んでいただきありがとうございました!!

参考記事

46
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
19