はじめに
初投稿です。
===========
あるテーブルのレコード取得時に関連テーブルの情報も取得しようとした場合に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 |
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) |
class Department extends Model
{
use HasFactory;
public $timestamps = false;
public function employees()
{
return $this->hasMany('App\Models\Employee');
}
}
LazyLoadの例
EagerLoadが関連するエンティティを事前にロードするのに対して、LazyLoadが必要なタイミングでクエリを発行する仕組みであるという前提ががある。
ということは、そのリレーション先のプロパティを使おうとすると自動的にロードされているはず。
$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()などのメソッドである。ここであえて例外を発生させてスタックトレースを追うというやり方で検証する。
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を参照する。
\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メソッドだけを加える。
\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クラス)にて実装されている模様。