Laravel Eloquent ORM基礎
EloquentはLaravelの「データベース翻訳者」です—PHPオブジェクトで対話し, それをSQLクエリに翻訳して実行します。
1. 学ぶ内容
- モデルの作成と属性定義:$fillable/$guarded/$casts/$attributes
- 完全なCRUDワークフロー:create/all/find/update/deleteと一括代入
- クエリビルダ:where/orderBy/groupBy/サブクエリ
- コレクション操作:filter/map/reduce/eachのチェーン処理
- ソフトデリートとリカバリ:SoftDeletesトレイト
2. フルスタック開発者の実話
(1) 悩み:SQL文字列連結がインジェクションとデータ損失を引き起こす
初期の頃, BobはShopMetricsをネイティブPHPで書いていました—すべてのSQLクエリは文字列連結で構築されていました:"SELECT * FROM shops WHERE id = " . $_GET['id']。ハッカーがAliceのストアIDに1 OR 1=1を注入し, サイト全体のデータ漏洩を引き起こしました。より一般的な問題は, BobがUPDATEクエリでWHERE句を忘れ, 1つのコマンドで全ストアの売上をゼロにリセットし, データの復元に丸1日かかったことでした。
(2) Eloquent ORMによる解決策
EloquentはPHPオブジェクトを使ってデータベースと対話し, 自動パラメータバインディングでインジェクション攻撃を防ぎ, 一括代入で機密フィールドを保護し, ソフトデリートで誤削除を防ぎます。
// 安全, 読みやすい, SQLインジェクションの可能性なし
$shop = Shop::create([
'name' => 'Alice Store',
'tenant_id' => 1,
]);
// 一括代入保護 — $fillableフィールドのみ許可
protected $fillable = ['name', 'slug', 'tenant_id'];
// revenueは$fillableにない — create()では設定不可
(3) 成果
BobがEloquentを使い始めた後, SQLインジェクションのリスクは排除され, 誤って削除されたデータはソフトデリートでワンクリック復元できるようになりました。コード量は200行のSQLから30行のPHPに削減されました。
3. モデル定義
(1) モデルの作成
php artisan make:model Shop
# 作成されるファイル: app/Models/Shop.php
# マイグレーション付き
php artisan make:model Shop -m
# 作成されるファイル: app/Models/Shop.php + database/migrations/create_shops_table.php
(2) モデルプロパティ設定
// app/Models/Shop.php
class Shop extends Model
{
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $guarded = ['id']; // 代替案:特定フィールドをブロック
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
protected $casts = [
'revenue' => 'decimal:2',
'is_active' => 'boolean',
'metadata' => 'json',
'launched_at' => 'datetime',
];
}
| プロパティ | 機能 | 推奨方法 |
|---|---|---|
$fillable |
一括代入を許可するフィールド | ✅ ホワイトリスト |
$guarded |
一括代入を禁止するフィールド | ❌ ブラックリスト |
$casts |
自動型変換 | 必須 |
$attributes |
フィールドのデフォルト値 | DBデフォルト値の代替 |
(3) Eloquentモデルクラス関係図
classDiagram
class Model {
+save()
+delete()
+update(array data)
+fresh()
+refresh()
+toArray()
+toJson()
}
class Shop {
+array fillable
+array casts
+tenant()
+orders()
+products()
}
class SoftDeletes {
+forceDelete()
+restore()
+trashed()
+withTrashed()
+onlyTrashed()
}
Model <|-- Shop
Shop ..|> SoftDeletes : uses trait
(1) ▶ サンプル:ShopMetricsショップモデル
// app/Models/Shop.php
class Shop extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $casts = [
'revenue' => 'decimal:2',
'metadata' => 'array',
];
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'active');
}
}
出力:
// 実行成功
4. CRUD操作
(1) 作成
// 方法1: create()で一括代入
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Alice Store',
'slug' => 'alice-store',
]);
// 方法2: new + save
$shop = new Shop();
$shop->tenant_id = 1;
$shop->name = 'Alice Store';
$shop->slug = 'alice-store';
$shop->save();
// 方法3: firstOrCreate — 検索または作成
$shop = Shop::firstOrCreate(
['slug' => 'alice-store'], // 検索条件
['name' => 'Alice Store', 'tenant_id' => 1], // 作成時の値
);
// 方法4: updateOrCreate — 更新または作成
$shop = Shop::updateOrCreate(
['slug' => 'alice-store'],
['name' => 'Alice Store Updated', 'revenue' => 5000],
);
(2) 読み取り
// 主キーで検索
$shop = Shop::find(1);
$shop = Shop::findOrFail(1); // 見つからない場合は404をスロー
// カラムで検索
$shop = Shop::where('slug', 'alice-store')->first();
$shop = Shop::whereSlug('alice-store')->firstOrFail();
// 全件取得
$shops = Shop::all();
$shops = Shop::active()->get(); // スコープを使用
// 大規模データセットのチャンク処理
Shop::chunk(200, function ($shops) {
foreach ($shops as $shop) {
// 200件ずつ処理
}
});
(3) 更新
// 単一モデルの更新
$shop->update(['name' => 'New Name']);
// クエリによる更新
Shop::where('status', 'suspended')->update(['status' => 'active']);
// 増減
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);
(4) 削除
// ソフトデリート (deleted_atを設定)
$shop->delete();
// 強制削除 (永久)
$shop->forceDelete();
// ソフトデリートの復元
$shop->restore();
// 削除済みを含めてクエリ
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();
(1) ▶ サンプル:ShopMetrics完全CRUDワークフロー
// 商品付きでショップを作成
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Bob Electronics',
'slug' => 'bob-electronics',
]);
$shop->products()->createMany([
['name' => 'Widget A', 'sku' => 'W-001', 'price' => 29.99],
['name' => 'Widget B', 'sku' => 'W-002', 'price' => 49.99],
]);
// Eagerローディングで読み取り
$shop = Shop::with('products')->whereSlug('bob-electronics')->firstOrFail();
// ショップと商品を更新
$shop->update(['revenue' => 15000]);
$shop->products()->whereSku('W-001')->update(['price' => 34.99]);
// ソフトデリートと復元
$shop->delete();
Shop::withTrashed()->whereSlug('bob-electronics')->first()->restore();
出力:
// 実行成功
5. クエリビルダ
(1) 条件クエリ
$shops = Shop::where('status', 'active')
->where('revenue', '>', 1000)
->orWhere(function ($query) {
$query->where('status', 'new')
->where('created_at', '>', now()->subDays(7));
})
->get();
// 動的where
$shops = Shop::whereStatus('active')
->whereRevenueGreaterThan(1000)
->get();
(2) 並び替え, グループ化, ページネーション
// OrderBy
$shops = Shop::orderBy('revenue', 'desc')->get();
// GroupByとhaving
$revenueByStatus = Shop::select('status', DB::raw('SUM(revenue) as total'))
->groupBy('status')
->having('total', '>', 1000)
->get();
// ページネーション
$shops = Shop::where('tenant_id', 1)->paginate(15);
$shops = Shop::where('tenant_id', 1)->simplePaginate(15);
$shops = Shop::where('tenant_id', 1)->cursorPaginate(15);
| ページネーション方法 | 実行クエリ | 用途 |
|---|---|---|
paginate() |
COUNT + SELECT | 総ページ数が必要な場合 |
simplePaginate() |
SELECTのみ | 総ページ数が不要な場合 |
cursorPaginate() |
WHERE付きSELECTのみ | 大規模データセットで最も効率的 |
(3) サブクエリ
// select内のサブクエリ
$shops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('SUM(total)')
->whereColumn('shop_id', 'shops.id'),
'orders_total'
)
->get();
// where内のサブクエリ
$latestOrders = Shop::where('created_at', function ($query) {
$query->selectRaw('MAX(created_at)')
->from('orders')
->whereColumn('shop_id', 'shops.id');
})->get();
(1) ▶ サンプル:ShopMetrics複雑クエリ
// 現在のテナントで売上が上位10店舗を, 注文数付きで取得
$topShops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('COUNT(*)')
->whereColumn('shop_id', 'shops.id')
->where('created_at', '>=', now()->subDays(30)),
'recent_orders_count'
)
->where('tenant_id', tenant()->id)
->where('status', 'active')
->orderBy('revenue', 'desc')
->take(10)
->get();
出力:
// 実行成功
6. コレクション操作
Eloquentのget()はCollectionオブジェクトを返し, 配列より強力なチェーンメソッドを提供します。
| メソッド | 機能 | SQL相当 |
|---|---|---|
filter() |
フィルタリング | WHERE |
map() |
マッピング変換 | SELECT変換 |
sortBy() |
ソート | ORDER BY |
groupBy() |
グループ化 | GROUP BY |
sum() |
合計 | SUM() |
count() |
カウント | COUNT() |
pluck() |
カラム抽出 | SELECT 1カラム |
unique() |
重複排除 | DISTINCT |
each() |
反復実行 | — |
reduce() |
累積計算 | — |
(1) ▶ サンプル:ShopMetricsコレクションチェーン操作
// テナントの全ショップを取得し, フィルタと変換
$topShops = Shop::where('tenant_id', 1)
->with('products')
->get()
->filter(fn ($shop) => $shop->revenue > 1000)
->sortByDesc('revenue')
->map(fn ($shop) => [
'name' => $shop->name,
'revenue' => $shop->revenue,
'product_count' => $shop->products->count(),
])
->take(10);
// ステータス別にショップをグループ化してカウント
$shopsByStatus = Shop::where('tenant_id', 1)
->get()
->groupBy('status')
->map(fn ($group) => $group->count());
// ['active' => 15, 'suspended' => 2, 'closed' => 1]
// 一括操作用にIDを抽出
$shopIds = Shop::where('status', 'active')->pluck('id');
// [1, 2, 5, 8, 12]
出力:
// 実行成功
7. ソフトデリート
(1) ソフトデリートの有効化
// モデル
class Shop extends Model
{
use SoftDeletes;
protected $casts = [
'deleted_at' => 'datetime',
];
}
// マイグレーション
$table->softDeletes(); // deleted_at TIMESTAMP NULLを追加
(2) ソフトデリート操作
// 削除 (ソフト — deleted_atを設定)
$shop->delete();
// 削除済みか確認
$shop->trashed(); // ソフトデリート済みならtrue
// 削除済みレコードを含める
Shop::withTrashed()->get();
// 削除済みレコードのみ
Shop::onlyTrashed()->get();
// 復元
$shop->restore();
// 永久削除
$shop->forceDelete();
(1) ▶ サンプル:ShopMetricsソフトデリートリカバリシナリオ
// Aliceが誤ってショップを削除
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();
// Bobは削除済みレコードから見つけられる
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();
// 関連データがすべてそのままの状態でショップを復元
if ($trashed) {
$trashed->restore();
// $trashed->productsもまだ存在 — 削除されていない
}
出力:
// 実行成功
8. 総合例:ShopMetrics注文分析
// ============================================
// 総合例: ShopMetrics注文分析
// 対象: CRUD, クエリ, コレクション, ソフトデリート, スコープ
// ============================================
// app/Models/Order.php
class Order extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'shop_id', 'user_id', 'order_number',
'subtotal', 'discount', 'total', 'status', 'metadata',
];
protected $casts = [
'total' => 'decimal:2',
'metadata' => 'array',
'deleted_at' => 'datetime',
];
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function scopeCompleted(Builder $query): Builder
{
return $query->where('status', 'completed');
}
public function scopeThisMonth(Builder $query): Builder
{
return $query->whereBetween('created_at', [
now()->startOfMonth(), now()->endOfMonth(),
]);
}
}
// 分析クエリ — 月次売上レポート
$monthlyReport = Order::where('tenant_id', tenant()->id)
->completed()
->thisMonth()
->with('shop')
->get()
->groupBy('shop.name')
->map(fn ($orders) => [
'shop' => $orders->first()->shop->name,
'order_count' => $orders->count(),
'revenue' => $orders->sum('total'),
'avg_order' => $orders->avg('total'),
])
->sortByDesc('revenue')
->values();
❓ よくある質問
findOrFailはいつ使うべきですか?findを使用してnullをチェックしてください。WHEREでフィルタリング), Collectionはメモリ上で動作します (PHPのfilterでフィルタリング)。大規模データセットはクエリレベルでフィルタリングし, 小さな結果セットにはCollectionメソッドを使用してください。deleted_atフィールドを設定するだけで, 関連データはデータベースに残ります。親モデルが復元されると, リレーションはすぐに利用可能になります。カスケードソフトデリートが必要な場合は, モデルのboot()メソッドでdeletingイベントをリッスンできます。DB::table())を使用してください。Eloquentは本質的にQuery Builderのラッパーです。Shop::where(...)->update([...])は単一のSQL文で一致する全レコードを更新するため非常に効率的です。$shops->each->update([...])はSQL文を1つずつ実行し, モデルイベントをトリガーします。イベントのトリガーが必要な場合は個別更新を使用してください。📖 まとめ
- EloquentはPHPオブジェクトでデータベースと対話し, 自動パラメータバインディングでSQLインジェクションを防止します
- $fillableホワイトリストが一括代入を保護し, $castsが自動型変換を実行します
- CRUDの4ステップ:create/read/update/delete,
findOrFailは404を返します - クエリビルダはWHERE, ORDER BY, GROUP BY, サブクエリをサポートします
- Collectionは配列より強力なチェーン操作 (filter/map/sortBy)を提供します
- ソフトデリートは
deleted_atでマークし, 永久削除ではなく, 復元をサポートします
📝 練習問題
-
基本問題 (⭐):ShopMetricsの
Productモデルを作成し,$fillableと$castsを定義し, 完全なCRUD操作 (作成, 読み取り, 更新, 削除)を実装し, Tinkerで各操作を検証してください。 -
応用問題 (⭐⭐):現在のテナントで売上が最も高い5つのストアを, 注文数付きで取得するクエリを記述してください。
selectSubサブクエリとCollectionのmapメソッドを使用して出力をフォーマットしてください。 -
チャレンジ (⭐⭐⭐):Orderモデルにソフトデリートとカスケード復元を実装してください。Orderが削除された時, そのOrderItemsもソフトデリートされ, Orderが復元された時, OrderItemsも一緒に復元されるようにしてください。モデルイベントリスナーを使って実装してください。



