404 Not Found

404 Not Found


nginx

Laravel Eloquent ORM基礎

EloquentはLaravelの「データベース翻訳者」です—PHPオブジェクトで対話し, それをSQLクエリに翻訳して実行します。

1. 学ぶ内容


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オブジェクトを使ってデータベースと対話し, 自動パラメータバインディングでインジェクション攻撃を防ぎ, 一括代入で機密フィールドを保護し, ソフトデリートで誤削除を防ぎます。

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) モデルの作成

BASH
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) モデルプロパティ設定

PHP
// 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モデルクラス関係図

100%
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ショップモデル

PHP
// 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');
    }
}

出力:

TEXT
// 実行成功

4. CRUD操作

(1) 作成

PHP
// 方法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) 読み取り

PHP
// 主キーで検索
$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) 更新

PHP
// 単一モデルの更新
$shop->update(['name' => 'New Name']);

// クエリによる更新
Shop::where('status', 'suspended')->update(['status' => 'active']);

// 増減
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);

(4) 削除

PHP
// ソフトデリート (deleted_atを設定)
$shop->delete();

// 強制削除 (永久)
$shop->forceDelete();

// ソフトデリートの復元
$shop->restore();

// 削除済みを含めてクエリ
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();

(1) ▶ サンプル:ShopMetrics完全CRUDワークフロー

PHP
// 商品付きでショップを作成
$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();

出力:

TEXT
// 実行成功

5. クエリビルダ

(1) 条件クエリ

PHP
$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) 並び替え, グループ化, ページネーション

PHP
// 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) サブクエリ

PHP
// 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複雑クエリ

PHP
// 現在のテナントで売上が上位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();

出力:

TEXT
// 実行成功

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コレクションチェーン操作

PHP
// テナントの全ショップを取得し, フィルタと変換
$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]

出力:

TEXT
// 実行成功

7. ソフトデリート

(1) ソフトデリートの有効化

PHP
// モデル
class Shop extends Model
{
    use SoftDeletes;

    protected $casts = [
        'deleted_at' => 'datetime',
    ];
}

// マイグレーション
$table->softDeletes(); // deleted_at TIMESTAMP NULLを追加

(2) ソフトデリート操作

PHP
// 削除 (ソフト — deleted_atを設定)
$shop->delete();

// 削除済みか確認
$shop->trashed(); // ソフトデリート済みならtrue

// 削除済みレコードを含める
Shop::withTrashed()->get();

// 削除済みレコードのみ
Shop::onlyTrashed()->get();

// 復元
$shop->restore();

// 永久削除
$shop->forceDelete();

(1) ▶ サンプル:ShopMetricsソフトデリートリカバリシナリオ

PHP
// Aliceが誤ってショップを削除
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();

// Bobは削除済みレコードから見つけられる
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();

// 関連データがすべてそのままの状態でショップを復元
if ($trashed) {
    $trashed->restore();
    // $trashed->productsもまだ存在 — 削除されていない
}

出力:

TEXT
// 実行成功

8. 総合例:ShopMetrics注文分析

PHP
// ============================================
// 総合例: 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();

❓ よくある質問

Q $fillableと$guardedの違いは何ですか?
A $fillableはホワイトリスト (これらのフィールドのみ一括代入を許可)で, $guardedはブラックリスト (これらのフィールドは禁止)です。$fillableホワイトリストの使用を推奨します—値の代入を許可するフィールドを明示的に宣言する必要があるため, より安全です。
Q findOrFailはいつ使うべきですか?
A レコードが見つからない場合に404ページを返す必要がある時に使用します。レコードが見つからないことが正常なビジネスロジックの一部 (結果なしの検索など)の場合は, findを使用してnullをチェックしてください。
Q Collectionメソッドとクエリビルダメソッドの違いは何ですか?
A クエリビルダはデータベースレベルで動作し (SQLのWHEREでフィルタリング), Collectionはメモリ上で動作します (PHPのfilterでフィルタリング)。大規模データセットはクエリレベルでフィルタリングし, 小さな結果セットにはCollectionメソッドを使用してください。
Q ソフトデリート後, モデルのリレーションはどうなりますか?
A ソフトデリートはdeleted_atフィールドを設定するだけで, 関連データはデータベースに残ります。親モデルが復元されると, リレーションはすぐに利用可能になります。カスケードソフトデリートが必要な場合は, モデルのboot()メソッドでdeletingイベントをリッスンできます。
Q EloquentとQuery Builderはどう使い分けるべきですか?
A モデルインスタンスが必要な場合 (リレーションへのアクセスやトレイトの使用)はEloquentを使用し, シンプルなクエリ結果だけでモデルが不要な場合はQuery Builder (DB::table())を使用してください。Eloquentは本質的にQuery Builderのラッパーです。
Q 一括更新と個別更新の違いは何ですか?
A Shop::where(...)->update([...])は単一のSQL文で一致する全レコードを更新するため非常に効率的です。$shops->each->update([...])はSQL文を1つずつ実行し, モデルイベントをトリガーします。イベントのトリガーが必要な場合は個別更新を使用してください。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):ShopMetricsのProductモデルを作成し, $fillable$castsを定義し, 完全なCRUD操作 (作成, 読み取り, 更新, 削除)を実装し, Tinkerで各操作を検証してください。

  2. 応用問題 (⭐⭐):現在のテナントで売上が最も高い5つのストアを, 注文数付きで取得するクエリを記述してください。selectSubサブクエリとCollectionのmapメソッドを使用して出力をフォーマットしてください。

  3. チャレンジ (⭐⭐⭐):Orderモデルにソフトデリートとカスケード復元を実装してください。Orderが削除された時, そのOrderItemsもソフトデリートされ, Orderが復元された時, OrderItemsも一緒に復元されるようにしてください。モデルイベントリスナーを使って実装してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%