🥇

[Laravel] クエリビルダでlatest()->first()やoldest()->first()と書いてはいけない

2022/10/05に公開

概要

『最新の1件取得』、『最も古い1件取得』という意図で latest()->first()oldest()->first() を使ってしまうとバグの原因になりかねないと感じたので、その根拠を示します。

理由

latest()ORDER BY created_at DESC を発行します。

『新しいレコード順に並べる』 と抽象的に理解して使ってしまいそう(それは正しいのだけど)ですが、実際にはバルクインサートして同時に作成したデータや factory で複数件作成したデータ、何かしらのマイグレーションスクリプトなどで生成されたデータなど、複数件のレコードが同じ created_at であるケースも考えられます。

ですので、 latest()->first() だけでは『最新の1件取得』を試みた際に created_at が同じレコード中のどのレコードが返されるか制御できません。

このときどのレコードが返されるかは DB によるし、取得する where の条件などの SQL によるし、実装によります。
MySQL の場合、デフォルトで primary key の昇順でソートされるので、 latest()->first() とすると、 created_at でソートした中の id の最も小さい1件が返されます。

多くの場合、『最新の1件取得』を試みたときに複数件あるうちの id が小さい方が返されるとは想像しづらいのではないでしょうか。これを理解せずに実装してしまうとバグの原因になりかねません。

対処法

# example1
# bad
Model::latest()->first();

# example2
# not bad
Model::latest()->orderBy('id', 'DESC')->first();

# example3
# good
Model::orderBy('created_at', 'DESC')->orderBy('id', 'DESC')->first();

example3 よりも example2 の方がコードが短くて済みますが、 example3 の方が少しコードは長くなってしまいますが、パっと見で挙動を正確に理解しやすいので個人的には好きです。

参考記事

ReadDouble > Laravel 9.x データベース:クエリビルダ > latestとoldestメソッド

Discussion