4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】LazyLoadとEagerLoadの動作の違い

Posted at

はじめに

初投稿です。

===========

あるテーブルのレコード取得時に関連テーブルの情報も取得しようとした場合にN+1という問題が起こりうる。

このN+1問題自体はよく知られているもので、LaravelのEloquentにおいてはwithメソッドを使ってEagerLoadingを行うことが解決策として示されている。

ここでwithメソッドを使わない通常のロード(以下LazyLoad)とEagerLoadの振る舞いの違いが気になって調べてみたので、学習した内容を整理する。

今回使用するモデル

以下のような従業員(employees)テーブルと所属部署(department)テーブルを用意する。従業員テーブル内のdepartment_idをキーとして、所属部署と従業員に1対多の関係を定義する。

▼従業員

column_name type
id pk, ai, unsignedBigInteger
last_name varchar(45)
first_name varchar(45)
department_id unsignedBigInteger
Employee.php
class Employee extends Model
{
    use HasFactory;

    public $timestamps = false;

    public function department()
    {
        return $this->belongsTo('App\Models\Department');
    }
}

▼所属部署

column_name type
id pk, ai, unsignedBigInteger
name varchar(45)
Department.php
class Department extends Model
{
    use HasFactory;

    public $timestamps = false;

    public function employees()
    {
        return $this->hasMany('App\Models\Employee');
    }
}

LazyLoadの例

EagerLoadが関連するエンティティを事前にロードするのに対して、LazyLoadが必要なタイミングでクエリを発行する仕組みであるという前提ががある。

ということは、そのリレーション先のプロパティを使おうとすると自動的にロードされているはず。

hoge.php
        $employee = Employee::first();
        $department = $employee->department;
        var_dump($employee);
object(App\Models\Employee)#1362 (30) {
  ["attributes":protected]=>
  array(4) {
    ["id"]=>
    int(1)
    ["last_name"]=>
    string(6) "佐藤"
    ["first_name"]=>
    string(6) "洋介"
    ["department_id"]=>
    int(1)
  }
  ["relations":protected]=>
  array(1) {
    ["department"]=>
    object(App\Models\Department)#1616 (30) {
      ["attributes":protected]=>
      array(2) {
        ["id"]=>
        int(1)
        ["name"]=>
        string(6) "営業"
      }
    }
  }
}

かなり端折っているがリレーション先のプロパティを参照した後に、Employeeモデルのインスタンスをdumpするとリレーション先のdepartmentsの情報を持っていてアクセスできる状態になっている。

【関連】LazyLoadを実現する仕組み

上記のLazyLoadにおいてはなぜ事前に読み込まれていないプロパティを参照してクエリを発行できるのか気になったので補足。

リレーション先のレコードを取り出すクエリを発行するのは、モデル内に定義されたメソッド内のbelognsTo()やHasMany()などのメソッドである。ここであえて例外を発生させてスタックトレースを追うというやり方で検証する。

emplyee.php
class Employee extends Model
{
    use HasFactory;

    public $timestamps = false;

    public function department()
    {
        throw new \Exception('hoge'); //追記
        return $this->belongsTo('App\Models\Department');
    }
}
#0 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php(574): App\\Models\\Employee->department()
#1 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php(526): Illuminate\\Database\\Eloquent\\Model->getRelationshipFromMethod('department')
#2 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php(451): Illuminate\\Database\\Eloquent\\Model->getRelationValue('department')
#3 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(2227): Illuminate\\Database\\Eloquent\\Model->getAttribute('department')
#4 /var/www/html/app/Console/Commands/XXX.php(yy): Illuminate\\Database\\Eloquent\\Model->__get('department')

例外を発生させた後にログに記載されたスタックトレースを追うと、LazyLoad時のModelインスタンスにはdepartmentが存在せず、phpのマジックメソッドである__get(存在しないプロパティにアクセスしたときに実行される)によってクエリ発行がトリガーされている。

LazyLoad実行時のクエリ

最初に従業員情報を4人分取得し、foreachを使ってリレーション先のdepartmentを参照する。

hoge.php
        \DB::enableQueryLog();
        $employees = Employee::take(4)->get();
        foreach ($employees as $key => $employee) {
            $employee->department;
        }
        dd(\DB::getQueryLog());

その結果、、、

select * from `employees` limit 4;
select * from `departments` where `departments`.`id` = 1 limit 1
select * from `departments` where `departments`.`id` = 4 limit 1
select * from `departments` where `departments`.`id` = 4 limit 1
select * from `departments` where `departments`.`id` = 3 limit 1

各エンティティについて関係テーブルを取得するためのクエリが発行されている。
最初に取得した従業員に部署ID=4で被ってしまったが含まれているが、重複に関係なしに実行されてしまう。

EagerLoadを使用した場合

先ほどのコードにwithメソッドだけを加える。

hoge.php
        \DB::enableQueryLog();
        $employees = Employee::with('department')->take(4)->get();
        foreach ($employees as $key => $employee) {
            $employee->department;
        }
        dd(\DB::getQueryLog());

その結果、、、

select * from `employees` limit 4;
select * from `departments` where `departments`.`id` in (1, 3, 4);

最初に取得したエンティティのIDだけ配列化し、SQLのWHERE~IN句を使ってリレーション先のエンティティを全て取得しておき、その後リレーション元テーブルのモデルとリレーション先のモデルコレクションを結合している。

おそらくilluminate¥Database¥Eloquent¥Builder.phpにおいて、

1. get()
2. eagerLoadRelations()
3. eagerLoadRelation()
4. match()

の順に実行されており、matchメソッドにて元テーブルとリレーション先のテーブルを外部キーで結合していると思われる。
matchメソッドについてはRelationクラスを継承した各リレーション処理を記述したクラス(今回であればBelongsToクラス)にて実装されている模様。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?