Laravelでデータベースを触る道は2本あります。DBファサードから組み立てるクエリビルダと、テーブルの1行をPHPのオブジェクトとして扱うEloquentです。Eloquentは「モデルクラス1つがテーブル1つ、インスタンス1つがレコード1行」という対応を作るActive Record型のORM。この記事ではモデルの作り方と規約、リレーションの定義、withによるeager loading、クエリビルダへ落とす判断ラインまでを、2026年8月29日時点の13.xのソースと公式ドキュメントで確認しました。
まとめ:Eloquentの担当範囲とクエリビルダとの線引き
先に結論を置きます。版に依存する記述は13.xのソースと公式ドキュメントで確認しました。
- Eloquentは「レコード1行=モデル1インスタンス」の対応を作るORMで、型変換・リレーション・論理削除を1クラスに集約します
- モデルは規約に従うだけなら空のクラスで動きます。規約を外すときは、従来のプロパティか13系のPHP属性で明示します
- リレーションはメソッドで定義し、プロパティとして読むと遅延で取得されます。この遅延が発行クエリを増やすため、一覧処理では
withで先読みします - 1件ずつの参照と更新はEloquent、数万件の一括更新や集計はクエリビルダ。インスタンス化が効かない処理では利点が消えます
- 既存DBが規約から外れている案件では、テーブルを作り直すより属性で上書きするほうが引き継ぎコストは下がります
Eloquentとは何か:Active Recordを採る理由とクエリビルダとの構造差
Eloquentの位置づけを、返るオブジェクトの違いから押さえます。ここが曖昧だと後半の判断が効きません。
ORMとしてのEloquentが担う変換とモデル1行1インスタンスの対応
ORMは、データベースの行とプログラム上のオブジェクトを相互に変換する層を指します。Eloquentが採るActive Recordパターンでは、変換の責務をモデルクラス自身が持ちます。App\Models\Flightがflightsテーブルに対応し、Flight::find(1)で得たインスタンスがidが1の行そのものになる、という対応です。設定量が少ない代わりに、モデルが永続化とドメインの知識を同時に抱えます。
クエリビルダが返すstdClassとEloquentが返すモデルの違い
同じSELECTでも、返ってくる中身が違います。DB::table('flights')->get()が返すのはstdClassの集合で、列の値はデータベースから来たままの型。datetimeの値は文字列のままです。
いっぽうFlight::all()が返すのはIlluminate\Database\Eloquent\Collectionで、要素はモデルのインスタンス。castsでbooleanと宣言していれば1はtrue、datetimeならCarbonとして受け取れます。取り出したあとのコレクション操作は両者で共通で、mergeやcontainsの挙動差はCollectionの値の追加と検索を扱った解説に整理してあります。
モデルクラスの作り方と13系で増えたPHP属性による規約の上書き
モデルはapp/Modelsに置き、Illuminate\Database\Eloquent\Modelを継承します。生成はArtisanコマンドで行います。
make:modelのオプションで同時生成できるファイルの組み合わせ
単体で作るならphp artisan make:model Flight。実務では周辺クラスをまとめて作ると手戻りが減ります。
-m:マイグレーションを同時に作る。テーブル定義から始めるときの既定の組み合わせ-mfsc:マイグレーション・ファクトリ・シーダー・コントローラを一度に作る--all:上記にポリシーとフォームリクエストを加えた全部入り
生成されたマイグレーションでの列定義は、マイグレーションの基本操作をまとめた記事を先に読むと流れが掴めます。
テーブル名・主キー・タイムスタンプで効く既定規約と個別の外し方
Eloquentは、クラス名を複数形のスネークケースに変換したものをテーブル名とみなします。AirTrafficControllerならair_traffic_controllers。主キーはidで自動増分の整数、タイムスタンプはcreated_atとupdated_atが存在する前提で動きます。
規約から外れる場合は明示が要ります。従来はprotected $tableやpublic $timestampsといったプロパティ指定でしたが、13系ではクラスに付けるPHP属性でも書けます。Table属性が受け取るのはname・key・keyType・incrementing・timestamps・dateFormatの6引数。散らばっていた複数のプロパティを宣言の直上1行へ集約できます。
fillableとcastsで宣言する一括代入の保護と属性の型変換
一括代入の保護は、許可リストの$fillableか拒否リストの$guardedのどちらかで宣言します。13系では#[Fillable]属性でも記述でき、署名はarray|string ...$columns。厄介なのは、許可リストに無い属性が例外も出さず黙って捨てられる既定挙動です。AppServiceProviderでpreventSilentlyDiscardingAttributesを本番以外だけ有効にしておけば、捨てられた時点で例外になり開発中に気づけます。リクエスト側で受け付ける項目を絞る話はLaravelのバリデーションの解説へ分けました。
属性の型変換はcastsメソッドで宣言します。プロパティではなくメソッドが正式な記法で、キーに列名、値にキャスト型を並べた配列を返す形。array・boolean・datetime・immutable_datetime・decimal:2のような精度付き小数・encrypted・hashedのほか、AsVector::classのようなクラス指定も並びます。hashedを指定した列は代入時にハッシュ化されるため、パスワード列でハッシュ関数を呼ぶ処理が不要になります。
レコードの取得・保存・削除でEloquentが自動で面倒を見る範囲
同じ結果を返すメソッドが複数あるため、戻り値と例外の違いで選び分けます。
allとfindとfirstOrFailで変わる戻り値と例外の扱いの差
全件はFlight::all()、条件付きはFlight::where('active', 1)->get()で取ります。allはクエリを足せないため、絞り込みがあるならwhereから始めてください。1件取得では、find($id)とfirst()が見つからないときにnullを返すのに対し、findOrFailとfirstOrFailはModelNotFoundExceptionを投げ、404レスポンスへ変換されます。「無ければ404」を意図しているなら後者にすると、null判定の分岐が消えます。存在すれば取得し無ければ作るfirstOrCreateとの違いも押さえておきましょう。複数行をSQL1本で書き込むupsertまで含めた書き分けはLaravel upsertの使い方を扱った記事で整理しています。
createとsaveの違いと論理削除を有効にしたときの取得挙動
新規作成はFlight::create()に配列を渡す形で、これは一括代入の保護を通ります。1件を取ってから属性を変えてsave()した場合は、変更のあった列だけがUPDATE文に載り、updated_atも自動で更新されます。いっぽうFlight::where(...)->update([...])は1件もインスタンス化せずに1本のUPDATEを発行。速い代わりにイベントは発火せず、updated_atも配列に含めない限り更新されません。
削除も2通り。物理削除はインスタンスのdelete()、行を残すならSoftDeletesトレイトを使い、テーブルへdeleted_at列を足します。以降、削除は時刻の書き込みに置き換わり、通常のクエリからは自動で除外。除外された行を含めるにはwithTrashed()、復旧はrestore()です。ただし論理削除は一意制約と噛み合いません。削除済みの行が残るため、メールアドレスにユニーク制約を張ったままだと同じ値で再登録できない点に注意が要ります。
リレーション定義の種類とhasMany・belongsToMany の使い分け基準
リレーションはモデルのメソッドとして定義し、プロパティとして読み出します。呼んだ時点でクエリが走る仕組みです。
hasOne・hasMany・belongsToで決まる外部キーの置き場所
どのメソッドを使うかは、外部キーがどちらのテーブルにあるかで決まります。外部キーを持つ側がbelongsTo、持たれる側がhasOneまたはhasMany。bookingsにflight_idがあるならFlight側がhasMany、Booking側がbelongsToです。
| 関係 | 親側の定義 | 子側の定義 | 外部キーの位置 |
|---|---|---|---|
| 1対1 | hasOne | belongsTo | 子テーブル |
| 1対多 | hasMany | belongsTo | 子テーブル |
| 多対多 | belongsToMany | belongsToMany | 中間テーブル |
| 中間経由 | hasManyThrough | 定義なし | 末端テーブル |
| 多態1対多 | morphMany | morphTo | 型列とID列の組 |
| 多態多対多 | morphToMany | morphedByMany | 多態中間テーブル |
外部キー名は「メソッド名のスネークケースにアンダースコアとidを足したもの」が既定。外れる列名なら第2引数と第3引数で明示します。表の下2行のポリモーフィックは型を表す文字列列とID列の組で参照するため外部キー制約が張れず、参照整合性をDB側で担保したい案件では個別の中間テーブルを作るほうが安全です。
belongsToManyの中間テーブル命名規約とpivot列の追加手順
多対多では中間テーブルの名前が自動で決まります。2つのモデル名を単数スネークケースにしてアルファベット順に並べ、アンダースコアで繋いだもの。UserとRoleならrole_userで、user_roleではない点でつまずきがちです。
中間テーブルに付加情報を持たせる場合はwithPivotに列名を列挙し、タイムスタンプも持たせるならwithTimestampsを続けます。取得後は関連モデルのpivotプロパティ経由で参照。付け外しはattachとdetach、指定した集合へ揃えるsyncで使い分けます。中間にもう1つテーブルを挟むhasManyThroughは階層が深いほどJOINが重くなるため、3階層を超えるなら中間に集計列を置く非正規化を選びます。
リレーションを条件で絞るwhereHasとwithの役割の食い違い
ここは混同しやすい部分です。withは「関連を一緒に読み込む」指定で親の件数は変わらず、whereHasは「その関連を持つ親だけに絞る」指定で親の件数が変わります。予約のある便だけを一覧したいならwhereHas、便の一覧に予約も並べたいならwithです。
両方が必要なら2つを繋げます。なおwithにクロージャで条件を足しても親は絞られません。関連側の絞り込み条件を何度も書くようなら、条件そのものをスコープへ切り出す手があります。書き方はクエリスコープの使い方をまとめた記事にまとめました。
withとloadによるeager loadingと自動読み込みの設定判断
リレーションは既定で遅延読み込みです。これが一覧処理で問題を起こすため、先読みの仕組みが用意されています。
withが発行クエリを2本へ収束させる仕組みとネスト指定の記法
便を100件取り、それぞれの航空会社名を表示する処理を素直に書くと、便のSELECTが1本と航空会社のSELECTが100本走ります。Flight::with('airline')->get()と書けば、航空会社をIN句でまとめて引くSELECTが1本走るだけ。合計2本へ収まります。取得済みの関連はメモリ上で親に紐づけ直され、ループ内の参照はクエリを発行しません。
複数指定は配列で並べ、ネストはドット記法でwith('airline.contacts')と書きます。読み込み時に条件を足すならクロージャを渡し、関連側にwhereを重ねる形。発行クエリ数の増え方と検出ツールについてはN+1問題の検出方法とORM別の対策へ分けて書いてあります。
取得後に足すloadと集計を足すwithCountの使い分けの基準
先読みを指定し忘れたまま取得した場合は、コレクションのloadで後から一括読み込みできます。条件分岐で読み込む関連が変わる処理なら、最初からloadで書くほうが素直です。
件数だけが欲しい場面で関連を丸ごと読むのは無駄になります。withCount('bookings')とするとbookings_count属性が生え、レコード本体は読み込まれません。同系統のwithSum・withMax・withExistsも同じ規則で名前が付きます。ただしselectで列を絞る指定と併用する場合は、selectを先に呼ばないと集計列が消えます。
遅延読み込みを止めるpreventLazyLoadingと自動化の設定差
先読みの書き忘れをレビューだけで防ぐのは現実的ではありません。13.xのModelにはpreventsLazyLoadingプロパティがあり、既定値はfalse。これをpreventLazyLoadingで本番以外だけ有効にすると、先読みしていない関連へ触れた瞬間に例外が飛びます。開発とテストでだけ検出できる設定です。
まとめて入れる入口もあります。shouldBeStrictは、この禁止に加えて一括代入から漏れた属性の検出と、取得していない属性へのアクセス検出を同時に有効化。逆方向の設定がautomaticallyEagerLoadRelationshipsで、関連へ触れた時点でまとめて読み込ませます。既存コードへ後から入れるなら後者、新規開発で規律を作るなら前者です。なおモデルの$withで常時読み込ませる書き方は、数百行の子を持つ関連やバッチで回すモデルでは裏目に出ます。
Eloquentを外してクエリビルダや生SQLへ落とす判断ライン
すべてをORMで書く方針は現場では続きません。どこで降りるかを先に決めると議論が短くなります。
一括更新・集計・大量削除でEloquentの利点が消えていく理由
Eloquentの利点は、行をオブジェクトとして扱えることに集約されます。裏を返すと、そう扱わない処理では利点がゼロになり、インスタンス化のコストだけが残る構図です。
10万件のフラグを立てる処理を1件ずつsave()で回すと、UPDATEが10万本走ります。同じことをクエリビルダのupdateで書けば1本。日付でグループ化した集計結果もモデルの形をしていないため、Eloquentを経由する意味がありません。ここは迷わず落とします。ただしモデルのイベントで在庫の引き当てや監査ログを実装している場合、一括更新はそれらを素通りする点に注意してください。
検索条件が動的に増える一覧画面でクエリビルダ側へ寄せる境界の引き方
業務システムの検索画面は、条件が10個以上並ぶことが珍しくありません。Eloquentでもwhenで条件を積めますが、JOINを4本も5本も重ねて集計列を並べ始めた時点で、返るモデルは実在のテーブル行と対応しなくなります。
境界の引き方は明快です。画面に出すためだけの横断的な集計結果はクエリビルダで取り、遷移先の詳細・更新画面はEloquentで扱う。一覧で得たIDから詳細側でモデルを取り直せば、更新処理はイベントも型変換も効いた状態で書けます。一覧のためにモデルへ表示専用の属性を足し続けると、モデルが画面の都合で汚れます。
生SQLへ落とす前に確かめるバインド変数の扱い方と可読性の代償
再帰CTEやウィンドウ関数など、クエリビルダで表現しきれないSQLはあります。DB::selectにプレースホルダ付きのSQLと値の配列を分けて渡せば、値は自動でエスケープされます。文字列連結で組み立てるとSQLインジェクションの入口になるため、ここだけは例外を作らない運用に。
代償は2つ。1つはデータベース製品への依存で、開発のSQLiteと本番のMySQLで方言が割れる点です。もう1つは、テーブル定義の変更がSQL文字列に届かないこと。列名を変えてもSQL文字列だけが取り残されます。生SQLはリポジトリクラス1箇所に集め、対象テーブルをコメントで残しておくと追跡が効きます。
受託開発でEloquentの規約を崩さずに運用するための実装ルール
複数人で数年触るコードベースの話です。Eloquentは自由度が高く、規律を先に決めないと個人差が保守コストになります。
規約から外れた既存DBへEloquentを後付けするときの妥協点
既存の業務システムを引き継ぐと、テーブル名が単数形だったり、主キーが商品コードのような文字列の複合キーだったりします。ここで「Eloquentに合わせて作り直す」と決めると、既存の帳票やバッチが全て巻き込まれます。
現実的な妥協点は、テーブルは触らずモデル側で全て明示すること。テーブル名・主キー・キーの型・自動増分の有無・タイムスタンプの有無は、13系ならTable属性1行で並べられます。ただし複合主キーは標準では扱えません。複合キーのテーブルは更新系だけクエリビルダで書く割り切りを先に決めるほうが安全です。既存DBへORMを載せる判断は要件定義の段階で効くため、業務用・Webアプリ開発のご相談では、テーブル定義の実物を見たうえで移行範囲を切り分けています。
モデルの肥大を防ぐ責務の切り分けと引き継ぎ前に決める4つの取り決め
Active Recordの弱点は、モデルが何でも置ける場所になりやすいこと。基準は2つです。クエリの条件はスコープへ、条件を組み合わせた業務判断はサービス層へ。「有効な便だけ」はスコープ、「予約可能かの判定」はサービスです。そして他のモデルを3つ以上参照し始めたメソッドはモデルから出す。参照が増えるほどテストの準備が重くなるためです。
あわせて、リレーションのメソッド名(1対多は複数形、1対1は単数形)、論理削除を使うテーブルの範囲、$fillableと$guardedのどちらへ寄せるかの3点を着手前に揃えます。前提となる版の期限も共有事項です。13系は2026年3月17日リリースでPHP 8.3から8.5を要求し、バグ修正は2027年第3四半期まで、セキュリティ修正は2028年3月17日まで。12系のバグ修正は2026年8月13日で終了済みです。版の確認手順はバージョン確認の記事にあります。
よくある質問
実装で繰り返し出てくる質問を5つ挙げます。
Eloquentとクエリビルダはどちらを先に覚えるべきですか?
クエリビルダを先に触ることをおすすめします。Eloquentはその上に乗っており、whereやorderByはそのまま共通。SQLとの対応を先に掴むと、Eloquentが何を自動化しているのかが見えます。逆順だと集計処理で手が止まりがちです。
モデルは必ずテーブルと1対1で作る必要がありますか?
1対1が既定ですが、必須ではありません。中間テーブルはPivotを継承したモデルにできますし、モデルを作らずDBファサードだけで扱うテーブルがあっても構いません。判断基準は「その行に振る舞いを定義したいか」。書き込むだけのログテーブルなら作らないほうがコード量が減ります。
withを付けたのにクエリが減らないのはなぜですか?
原因は3つあります。1つ目、ループの中でwithを書いている場合。先読みは親を取る時点で1回だけ効きます。2つ目がネストの指定漏れで、1階層目だけを書いても2階層目の参照でクエリが増えます。3つ目は、アクセサの内部で別の関連を参照しているケース。実測手順はN+1問題の記事に分けました。
fillableを空にしてguardedを空配列にしても問題ありませんか?
$guardedを空配列にするのは全列を一括代入可能にする指定で、リクエスト由来の配列をそのままcreateへ渡すと想定外の列まで書き換えられます。管理者フラグや状態を持つ列があるテーブルでは避けてください。入力元が信頼できる内部テーブルに限れば選択肢になります。
Laravel 13でEloquentの書き方は変わりましたか?
既存の書き方はそのまま動きます。13系で増えたのは選択肢のほうで、モデルの設定をPHP属性でも書けるようになりました。13.xのEloquent配下には24種類の属性クラスが置かれ、テーブル定義・一括代入・表示制御などをクラス宣言の直上へ集約できます。移行は必須ではなく、新規モデルから属性で書き始める進め方で問題ありません。
関連記事
- N+1問題とは?発行クエリ数の実測と検出方法・ORM別の対策:先読みが要る理由を実測の側から扱っています
- Laravel Collectionの値の追加と検索|push・merge・containsの挙動差:取得後の操作方法です
- Laravel migrationでenumを扱う実践ガイド:キャストの前段になる列定義の書き方です
- Laravelのバージョン確認|artisanとcomposerの違いとサポート期限:属性記法が使えるかの判断材料です