はじめに
Laravel学びたてはDBデータを扱う際はCollectionメソッドが非常に充実していることから
$users = User::all(); // 一旦Collectionで取得
$hoge = $users->map(function () {
// 何かしらの処理
});
みたいに一旦Collection取得→何かしら処理する、という無駄なDB取得クエリを書いてよく注意されていました。。。
そこでLaravelはSQLのSELECT句以外にもEXISITS句やCOUNT句などが使えるクエリビルダという機能がありますが、このクエリビルダ使う際に個人的に直感とは違った挙動を最近見つけたのでメモ。
具体的な挙動
よくあるクエリビルダの使用例
クエリビルダはクエリを発行するベースみたいなものなので例えば、
以下の様に$usersBuilder
というクエリビルダを作成し、count()
で対象レコード有無をチェックしてからget(['id', 'name'])
でid, nameを持つUserモデルが集まったCollctionを取得
なんてことは結構あると思います。
public function index()
{
$ids = [1, 2, 3, 4, 5, 6, 7, 8];
$usersBuilder = User::whereIn('id', $ids);
if ($usersBuilder->count() !== 0) {
$users = $usersBuilder->get(['id', 'name']);
}
return view('users.index', compact(['users']));
}
※あくまで例なのでわざわざCOUNT句のクエリ発行しなくてよくない?というお声は一旦無しでお願いします。。。
実際に直面した挙動
今回、※のところで追加機能の実装をします。
今回の機能追加するにあたって、既存の$usersBuilder->get(['name'])
で取得しているnameだけでは機能が足りないため、いっそこと$usersBuilder->get();
をしています。
$ids = [1, 2, 3, 4, 5, 6, 7, 8];
$usersBuilder = User::whereIn('id', $ids);
if ($usersBuilder->count() !== 0) {
// $usersBuilderがあるときの処理
...
}
$userNames = $usersBuilder->get(['name'])->map(function($user){
return $user->name;
});
// ※機能追加のため別でSelect分追加
$users = $usersBuilder->get();
return view('users.index', compact(['userName', 'users']));
ここの$users
で取得できる想定は以下のイメージのCollectionでした。
※実際のCollectionの形と違うかもですがイメージですのでご了承ください
$usersBuilder->get()
>>> \Illuminate\Database\Eloquent\Collection {
items: array(5) {
App/Models/User: {
...
attributes (4) {
id: 1,
name: hoge,
email: hoge@test,
password: ytmsi#ei2@
}
}
}
}
ただ実際はこうでした
$usersBuilder->get()
>>> \Illuminate\Database\Eloquent\Collection {
items: array(5) {
App/Models/User: {
...
attributes (1) {
name: hoge,
}
}
}
}
nameしかattribute(プロパティ)に入っていない…
原因
どうも$usersBuilder->get();
の前に実行している$usersBuilder->get(['name'])
によって取得できるプロパティが制限されてしまっているらしい。
対策
じゃあもういっその事、最初からget()
してしまえということでこうしました
$ids = [1, 2, 3, 4, 5, 6, 7, 8];
$usersBuilder = User::whereIn('id', $ids);
if ($usersBuilder->count() !== 0) {
// $usersBuilderがあるときの処理
...
}
// 追加:先にuserのプロパティ全て取得してくる
$users = get();
$userNames = $users->map(function($user){
return $user->name;
});
// $usersをbladeに渡す
return view('users.index', compact(['userName', 'users']));
まあ最初からそうしろよ、というお声も届きそうですが、そういう挙動もあるんだということが知れてよかったということで。。。笑
おわりに
当初の自分の書き方ではget()
を2回実行しているのでSELECTクエリが2回発行しているので無駄ですよね。
なのでLaravelもget()
が1つのクエリビルダから2回以上実行されることなんてあまり想定していないのかもしれない、とも思いました。
Laravelとしては
「え、1つのクエリビルダから2回もSELECT句実行するの?いいけど想定通り取得できないよ?挙動は保証しないよ?」
みたいな挙動でしょうか。
なので、get(['name'])
のようにカラムを指定してDBからのデータ取得量を制限できるのはいいかもしれないですが、今回の様に今後の機能追加時に結局「get()
でいいじゃん」ってことになることは多いかもですね。
個人的には後々の修正は簡単なので、最初はget(['name'])
などで取得カラム制限、必要に応じてget()
に切り替えるという形でいいのかなと思います。
最後までお読みいただきありがとうございました。