Laravelで知ってると差がつくEloquentとQuery Builder(+おまけで現場Tipsも)
「LaravelのEloquentとQuery Builderの違いって、なんとなくわかってるつもりだったけど説明できない」
そんな人、意外と多いと思います。私もそうでした。
同じように
「where
もget
も動くし、なんかgroupBy
とかsort
も使えるし」
「でもsave()
できたりできなかったりするんだよな」
というモヤモヤを感じたことがあるなら、この記事はたぶんちょうどいい。
んなもん知ってるわ!という方は、Switchでハデス2にハマりましょう。おすすめです。
Eloquent、Query Builder、Model、Collection。
全部Laravelでは“当たり前に使う”けど、それぞれの階層を理解するとコードの意味が一気にクリアになります。
・・・最近一緒に仕事をしている人がとても大人なLaravelコードを書いていたので悔しくて一生懸命調べましたw
EloquentとQuery BuilderとModelの関係
ざっくり言うと、三者の関係はこう。
Eloquent Model(Userなど)
└── Query Builderを内部的に利用
└── SQLを生成してDBに投げる
つまり:
- Model:テーブルを表すクラス(Userモデルなど)
- Eloquent:Modelを通してDBとやりとりするORM(Object Relational Mapper)
- Query Builder:SQLをオブジェクト的に組み立てるための仕組み
例を見てみよう。
// Query Builder
DB::table('users')->where('id', 1)->first();
// Eloquent ORM
User::where('id', 1)->first();
// Model経由
User::find(1);
どれも「ユーザーを1件取る」けど、
返ってくる中身が違う。
呼び出し方 | 戻り値の型 | 備考 |
---|---|---|
DB::table() | stdClass | 生のオブジェクト。save() できない |
User::where() | User(Eloquentモデル) |
save() できる/イベントも効く |
User::find() | User(Eloquentモデル) | 主キー検索のショートカット |
Query Builderは“素のSQL”に近く、
Eloquentは“ORM(オブジェクトマッピング)”層にいる。
同じように見えても扱う世界が違うわけです。
Query Builderでしかできないこと
Eloquentが便利なのは間違いないけど、
Query BuilderにはBuilderでしかできないことが結構あります。
複雑なJOIN・集計・サブクエリ
DB::table('users as u')
->join('orders as o', 'u.id', '=', 'o.user_id')
->select('u.name', DB::raw('COUNT(o.id) as orders'))
->groupBy('u.id')
->having('orders', '>', 3)
->get();
こういう集計系や複雑な結合は、
Eloquentでも書けなくはないけどBuilderの方が明快で高速。
一括更新・大量INSERT
DB::table('users')->insert([
['name' => 'Taro'],
['name' => 'Jiro'],
]);
Eloquent::create()
やsave()
だと1件ずつINSERTするので遅い。
バルク処理ならBuilder一択。
生SQLそのまま投げたいとき
DB::select('SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY');
こういうやむを得ないケースも、Query Builder側の領域。
Eloquentでしかできないこと
逆に、Eloquentの特権もたくさんある。
モデルイベント
protected static function booted()
{
static::creating(function ($user) {
$user->uuid = Str::uuid();
});
}
creating
, created
, updating
…などのイベントはBuilderにはない。
“DB変更のライフサイクルフック”が使えるのがEloquentの強み。
リレーション(hasMany / belongsTo など)
$user = User::find(1);
$posts = $user->posts;
Query BuilderだとJOINを自分で書く必要があるけど、
Eloquentならモデルの関係をそのままオブジェクトで扱える。
アクセサ / ミューテータ
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
$user->full_name
で自動的に結合される。
実データを“見せ方”として加工できるのはEloquentならでは。
CollectionはSQLを発行しない
Eloquentでget()
した後にgroupBy()
やsort()
しているコード、見たことないですか?
User::select()->get()->sort();
これ、SQLを発行していません。
get()
でDBアクセスが終わっていて、
sort()
はPHPのメモリ上でソートしているだけです。
大量件数だとこれがボトルネックになります。
ソートやグルーピングはDB側でやる方が圧倒的に速い。
// DBでソート
User::orderBy('created_at', 'desc')->get();
Collectionは便利だけど、
「SQLを出してるのか、メモリで動いてるのか」を意識しておきたい。
よくある混乱ポイント
first()
とget()
の違い
-
first()
→ モデル1件 or null -
get()
→ Collection(複数件)
get()->first()
は“2回フェッチしてる”ようなもので無駄です。
1件ならfirst()
でいい。
pluck()
の引数2つ
User::pluck('name', 'id');
pluck(value, key)
の順。
つまり [id => name]
の連想配列的Collectionになります。
※ Eloquentの場合は「Collection」、Query Builderの場合は「配列」になるので注意!
creating
でid
はまだ取れない
creating
イベントはINSERT前に発火するので、
$model->id
はまだ未確定です。
使いたいなら created
イベントへ。
tap()
は外の変数も使える
$count = 0;
User::query()
->tap(function ($q) use (&$count) {
logger($q->toSql());
$count++;
})
->get();
use
でキャプチャすれば普通に使える。
tap()
は「処理を挟んで同じインスタンスを返す」ための小技。
おまけ:現場で差がつくTips集
SQLを確認する
User::where('status', 'active')->toSql();
toSql()
は実行せずにSQL文字列を返す。
ログ出力にも便利。
条件のグルーピング
User::where(function($q) {
$q->where('active', true)
->orWhere('role', 'admin');
});
複雑なOR
を安全に書くならこれ。
地味だけどめっちゃ重要。
遅延ロードを検知する
Model::preventLazyLoading(! app()->isProduction());
N+1問題の温床を開発中に潰せる。
Eloquentの挙動を“見える化”しておくのはおすすめ。
hydrate()
でDB::select()の結果をModel化する
$rows = DB::select('SELECT * FROM users WHERE active = 1');
$users = User::hydrate(array_map(fn($r) => (array) $r, $rows));
これでsave()
やtoArray()
も使える。
JOIN結果にエイリアスを付けておけば安全に動く。
🧰 モデルイベントをもっと活用する
Eloquentの強みの一つが「イベントをフックできる」こと。
creating
や created
、updating
などは、DB操作の直前・直後に呼ばれます。
代表的なイベント一覧
イベント名 | タイミング | よく使う用途 |
---|---|---|
creating | INSERT前 | UUID発行、デフォルト値セット、バリデーション |
created | INSERT後 | 関連レコードの作成、通知発火、ログ出力 |
updating | UPDATE前 | 値の整形、変更検知 |
updated | UPDATE後 | 更新履歴の保存、外部API呼び出し |
deleting | DELETE前 | 関連データの削除確認 |
deleted | DELETE後 | 監査ログ、論理削除の補助 |
例1:creating で UUID を自動発行
protected static function booted()
{
static::creating(function ($model) {
if (! $model->uuid) {
$model->uuid = (string) Str::uuid();
}
});
}
creating
は INSERT 前に呼ばれるので、
「DBに入る前に属性を整える」処理を書くのに向いています。
例2:created で 関連レコードを自動生成
protected static function booted()
{
static::created(function ($user) {
Profile::create([
'user_id' => $user->id,
'nickname' => $user->name,
]);
});
}
created
は INSERT 後なので、自動採番の id
が確定しています。
「登録後に付随レコードを作る」や「通知を飛ばす」系はこっち。
例3:updating / updated で変更検知
protected static function booted()
{
static::updating(function ($user) {
if ($user->isDirty('email')) {
Log::info('Email changed', ['user_id' => $user->id]);
}
});
}
isDirty()
は変更があったカラムだけを検出できる便利メソッド。
更新トリガーで監査や履歴を残すときに使える。
🧩 tap() をちゃんと理解する
Laravelの tap()
は、
「処理の途中に副作用を挟みつつ、値そのものはそのまま返す」関数。
これがシンプルだけど非常に便利。
tap()
パターン1:グローバル関数 $user = tap(User::find(1), function ($u) {
$u->last_accessed_at = now();
$u->save();
});
tap()
は第1引数の値をコールバックに渡し、
その値自体を戻します。
副作用(ここでは更新)を行いながら変数を返すことができる。
return tap($user)->notify(new WelcomeMail);
↑ “メソッドチェーンを切らずに処理を挟む” のに使える。
tap()
メソッド
パターン2:Builderの User::query()
->tap(function ($query) {
logger($query->toSql());
})
->where('active', 1)
->get();
これは クエリビルダに対して副作用を差し込む 用。
tap()
の中ではSQLがまだ実行されていないので、
toSql()
や getBindings()
のような「組み立て中の確認」ができる。
パターン3:変数の初期化やトランザクションの補助にも
$user = tap(new User(['name' => 'Taro']), fn($u) => $u->save());
あるいはトランザクション内で:
DB::transaction(function () {
tap(Order::create([...]), function ($order) {
$order->items()->createMany([...]);
});
});
“結果を戻しながら何かする” というLaravelらしい文法糖。
書いてるうちにクセになります。
まとめ
- Query Builderは“SQL職人”向け。
→ JOIN・集計・バルク更新・速度優先。 - Eloquentは“オブジェクト指向的にDBを扱いたい人”向け。
→ モデルイベント・アクセサ・関連・キャストが強い。 - Collectionは“取得後のデータ整形”。
→ SQLは発行されない。
この3つを混同せず、
「今はどの層で動いているのか」を意識できると、
Laravelがぐっと読みやすく、チューニングしやすくなります。

あなたの情熱を確かなカタチに。 心躍る「クリエイティブ」に専念できる環境を!テクノロジーを駆使して誰かの創造性を爆上げする! ワンフレーム株式会社のエンジニアブログです! コーポレートサイトはこちら→ oneframe.co.jp/
Discussion