Laravel の Eloquent 検証(ローカルスコープ)
DB からデータを取得する時、様々な条件のもとに取得してくる事が多いと思います。
その度にwhere
をいくつも連結して実装するのも、読み解くのも大変ですよね。
何が大変かと言うと、結局その実装で取得されるデータが何を表しているのか分かりにくい、というのが原因の1つなのではないかと感じました。
そういった事を解決してくれるクエリスコープ
という機能の1つであるローカルスコープ
について検証しました。
参考:Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans
ローカルスコープの検証
マイグレーションファイルの準備
以下のような users テーブルを用意します。
- is_active カラム:有効なユーザーかどうかを表すフラグ
role カラム(※ 例として以下のような分類とします)
admin
:管理者prime
:一般よりも権限を持つユーザーuser
:一般ユーザー
マイグレーションの定義
<?php public function up() { Schema::create('users', function (Blueprint $table) { $table->string('id', 36)->primary(); $table->string('last_name'); $table->string('first_name'); $table->boolean('is_active'); $table->enum('role', ['admin', 'prime', 'user']); $table->timestamps(); }); }
テストデータの準備
今回は、Faker
を使い、10人分のテストデータを生成しました。
Faker
はランダムなテストデータを生成するのに便利なライブラリです。
以下のサンプルのように人の名前だけでなく、email
や住所
なども生成してくれます。
参考:[PHP] Fakerでランダムなフェイクデータを作成する - Qiita
<?php $roles = ['prime', 'user']; $faker = Factory::create('ja_JP'); foreach (range(1, 10) as $i) { // 最初の1人だけ admin にし、それ以外はランダムで prime か user ロール。 $role = $i === 1 ? 'admin' : $roles[rand(0, 1)]; User::create([ 'id' => $faker->uuid, 'last_name' => $faker->lastName, 'first_name' => $faker->firstName, 'is_active' => $faker->boolean(), 'role' => $role, ]); }
生成したデータは以下の通りです。
検証1:シンプルな定義(有効なユーザーを取得する)
scope
をプレフィックスとした function をモデルに定義する必要があります。
ただし、この function を呼び出す時は、scope
を付けません。
モデル定義
ここでは、有効状態のユーザー
を操作する頻度が多い事を想定し、
以下のように User モデルにscopeActive
という名前で function を追加します。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { 〜 略 〜 protected $casts = [ 'is_active' => 'boolean', ]; public function scopeActive(Builder $query): Builder { return $query->where('is_active', true); } }
実行
定義したスコープを使って件数を取得します(想定では8
人)。
<?php logger(User::Active()->count());
出力結果
[2019-05-06 08:59:09] testing.INFO: Query Time:15.99ms] select count(*) as aggregate from `users` where `is_active` = ? [2019-05-06 08:59:09] testing.INFO: array ( 0 => true, ) [2019-05-06 08:59:09] testing.DEBUG: 8
発行されるクエリがis_active
を条件にしている事、カウントも適切な値が取得できた事を確認できました。
少しハマったポイント
※ ドキュメントにはUser::active()
と、a
が小文字で書かれており、これを実行すると以下のようなエラーが発生しました。
試しにA
と大文字から書き始めたら正常に動くようになりました。
BadMethodCallException: Call to undefined method App\Models\User::acitve()
検証2:スコープに引数を渡せるようにする(動的スコープ)
モデル定義
role
がprime
もしくはuser
のリストを取得できるようにしたい、といったケースに対応できるようにするため、以下のようにscopeOfType
という function を追加します。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { 〜 略 〜 // $type には `admin`, `prime`, `user` のいずれかを指定します。 public function scopeOfType(Builder $query, string $type): Builder { return $query->where('role', $type); } }
実行
定義したスコープを使い、prime
なユーザー数数を取得します(想定では3
人)。
<?php logger(User::OfType('prime')->count());
出力結果
[2019-05-06 09:21:46] testing.INFO: Query Time:20.1ms] select count(*) as aggregate from `users` where `role` = ? [2019-05-06 09:21:46] testing.INFO: array ( 0 => 'prime', ) [2019-05-06 09:21:46] testing.DEBUG: 3
このように、role
カラムを対象にprime
で絞っており、結果が3
である事も確認できました。
もちろん、User::OfType
に'user'
を渡せば、role
を対象にuser
で絞る事ができます。
<?php logger(User::OfType('user')->count());
[2019-05-06 09:39:39] testing.INFO: Query Time:17.09ms] select count(*) as aggregate from `users` where `role` = ? [2019-05-06 09:39:39] testing.INFO: array ( 0 => 'user', ) [2019-05-06 09:39:39] testing.DEBUG: 6
検証3:作成したスコープの合わせ技
実行
作成した2つの function を合わせて使用し、アクティブでrole
がuser
のユーザー数を取得します。(想定は4
人)
合わせる時は単純に繋げて使用する事ができます。
<?php logger(User::Active()->OfType('user')->count());
出力結果
[2019-05-06 09:43:51] testing.INFO: Query Time:17.5ms] select count(*) as aggregate from `users` where `is_active` = ? and `role` = ? [2019-05-06 09:43:51] testing.INFO: array ( 0 => true, 1 => 'user', ) [2019-05-06 09:43:51] testing.DEBUG: 4
is_active
およびrole
カラムを対象に絞り、結果が4
である事を確認できました。
感想
スコープの名称(function 名)を適切なもので定義する事で、その実装がどういうデータを取得してこようとしているか、判断しやすくなるなと感じました。 むしろ判断しやすくなるようなスコープを定義する必要があるかもしれません。
また、例えばあるカラムの名前を変更する事になった場合、そのカラムを利用するクエリ(例:where('カラム名', $value)
)を、スコープの中に閉じ込めておくことで、ソースコードの修正も容易になるのかなと思いました。
スコープを積極的に使用して、メンテナンス性の高いモデルを実装していきたいなと思いました。