この記事は PHP 8.1.12、Laravel 9.52.9、MySQL 8.0.32 環境を元に書かれた記事です。
Laravel にはグローバルスコープという仕組みがあります。グローバルスコープとはモデルに対する全てのクエリに適用される再利用可能なクエリ制約のことです。これにより特定のモデルで常に適用したい条件を一か所にまとめることができ、コードの重複を防ぐことが可能です。例えばアクティブなユーザーだけを対象にしたい場合や、一定期間内に作成されたレコードだけを取り出したい場合などに有効です。
Eloquentの準備 9.x Laravel#グローバルスコープ
グローバルスコープは便利ですがエラーの原因となる時もあります。その一つがJOINが絡む場合です。次の例がそれです。
Account::addGlobalScope(static function(\Illuminate\Database\Eloquent\Builder $builder) { $builder->where('created_at', '>=', now()->subDays(30)); }); Account::query() ->leftJoin('companies', 'accounts.company_id', '=', 'companies.company_id') ->get(); // SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'created_at' in where clause is ambiguous // (SQL: select * from `accounts` left join `companies` on `accounts`.`company_id` = `companies`.`company_id` where `created_at` >= 2023-06-21 10:06:16 and `accounts`.`deleted_at` is null)
このエラーは「where 句の中にある created_at が曖昧です」というエラーメッセージが示すように、created_at というフィールド名が複数のテーブルに存在するため、SQLがどのテーブルのcreated_atを参照すべきか判断できないため発生します。これを解消する方法を二つ紹介します。
一つ目はそもそも JOIN をしないことです。Laravel にはリレーションを表現する機能があり、そちらを使うことで JOIN せずに複数のテーブルから関連する情報を取得できます。Laravel 的にもこちらの方がお作法に則っている形です。これは次の様に書けます。
Account::query()->with('company')->get();
二つ目の方法はグローバルスコープの中身が実行される際に動的にテーブル名をカラム名に付け加える方法です。 JOIN しなければならない場合でもこちらが使えます。これは次の様に書けます。
Account::addGlobalScope(static function(\Illuminate\Database\Eloquent\Builder $builder) { $builder->where($builder->getModel()->getTable() . '.created_at', '>=', now()->subDay()); });
クエリビルダから根元のモデル、モデルのテーブル名と引っ張ってきます。こうすることでグローバルスコープ実行時には自動で適切なテーブル名が入り、ambiguous と怒られることもなくなります。これを用いることで次の様に複数のモデルに同一のグローバルスコープを簡易に定義することもできます。
$resentCreatedScope = static function(\Illuminate\Database\Eloquent\Builder $builder) { $builder->where($builder->getModel()->getTable() . '.created_at', '>=', now()->subDay()); }; Account::addGlobalScope($resentCreatedScope); Company::addGlobalScope($resentCreatedScope); Post::addGlobalScope($resentCreatedScope);
この様にしてグローバルスコープを便利に使うことができます。