メシのタネ

メシのタネになる、Laravelや設計思想の技術配信サイト


Laravel Eloquent 超攻略ロードマップ — Active Record とリレーション沼を一気に突破


  1. Laravel
  2. Laravel Eloquent 超攻略ロードマップ — Active Record とリレーション沼を一気に突破
LaravelEloquentの全体を表した図

— ここでEloquent迷子は全員保護します—

どういうこと?

Eloquent を使ってると「え、ここって Active Record 的に太らせていい場所?」「pivot に追加カラム入れたら破裂したんだけど?」「syncしたらデータ消えたぞ!?」みたいな 首かしげポイント が必ず出てくる。

その都度ググって深夜に旅に出るのはしんどいので、自分が沼った順 に道標を置いておくページです。スクロールしながら “ここハマった!” と思った所でリンクを踏んでください。

アイス一本食べ終わる時間で読み終わるサイズにしてあるので、風呂上がりにでも肩の力抜いてどうぞ。

Eloquent とは? — Laravel 流 ORM の心臓部

EloquentはLaravel に標準搭載されたActive Record 型 ORMモデル=テーブル行というシンプルな対応づけで、 SQL を書かずに作成・更新・検索が完結します。

ここではモデル/リレーション/コレクション の3つの観点から “Eloquent の基本形” を 30 秒でザックリ把握しておきましょう。

Eloquent モデル:テーブル 1 枚=クラス 1 個


php artisan make:model Post

生成される app/Models/Post.php が「posts テーブル」の顔。

fillable / casts / scopeXxx属性・キャスト・共通クエリ を内包できるのがスタートラインです。

リレーション:hasOne / hasMany / belongsToMany


class Post extends Model
{
    public function author()   { return $this->belongsTo(User::class); }
    public function tags()     { return $this->belongsToMany(Tag::class); }
}

リレーションメソッド = クエリビルダの別名with('tags') で先読み、sync() で中間テーブルを同期——Eloquent が 人間語 API に翻訳します。

コレクション:取得後の「あと3行」をチェーンで


$activePosts = Post::latest()->get()          // Eloquent→Collection
    ->filter(fn ($p) => $p->isPublished())    // 絞り込み
    ->pluck('title');                         // 欲しいカラムだけ

map / filter / chunk など 配列ライクな後処理 が標準装備。SQL に戻らずビジネスロジックの最後のひと押しを書けるのが強みです。

立ち位置ざっくり確認 — Active Record と Data Mapper

「Eloquent=Active Record」は周知の事実。でも太りすぎたらどうしよう? そこで Data Mapper の考え方が出てくる。

ミニコード対比

Active Record (Eloquent)

$user = User::create(['name' => 'Yuzu']);     // モデル自身が INSERT
$user->posts()->create(['title' => 'Hi']);    // リレーションもモデル発

Data Mapper (Doctrine)

$user = new User('Yuzu');                     // モデルは純オブジェクト
$em->persist($user);                          // DB へは EntityManager が出動
$em->flush();

👉 どっち派? 踏み込んだ話はこちらhttps://mikaduki.info/laravel/eloquent-active-record-vs-data-mapper/

リレーションは線で覚えよう

モデルを書く前に紙に線を引く──失敗率が 3 割減ります。こんな感じ。

たねまる

あらかじめ予想しておかないと、後でアタマの疲労が三倍だよ〜!

pivot の正体は関係の倉庫

モデル同士は線路、pivot はその途中にある貨物ヤード。

線路だけ なら「A から B に届く」ことしか分からない。

でも貨物ヤードに “荷札” を貼れば──

荷札ピボットの追加カラム例何が分かる?
granted_by付与者 ID誰が 橋を渡したか
expires_at期限いつまで 有効か
role役割/数量どの荷物 を積んでいるか

最低限の設計 & サンプルコード

🚧 pivot テーブルを「倉庫」にする骨格

// migration: member_project pivot
Schema::create('member_project', function (Blueprint $t) {
    $t->foreignId('project_id')->constrained()->cascadeOnDelete();
    $t->foreignId('member_id')->constrained()->cascadeOnDelete();

    // 荷札(メタ情報)
    $t->string('role')->nullable();          // 役割ラベル
    $t->foreignId('granted_by')->nullable(); // 付与者 ID
    $t->timestamp('expires_at')->nullable(); // 賞味期限

    $t->timestamps();                        // created_at / updated_at
    $t->primary(['project_id','member_id']); // 同じ荷物は 1 行だけ
});

▲ “橋渡し”に荷札カラムを足して、複合主キーで二重梱包を防止

🏷️ モデル側で「倉庫の荷札」を宣言する

// app/Models/Project.php
class Project extends Model
{
    public function members()
    {
        return $this->belongsToMany(Member::class)
            ->withPivot(['role', 'granted_by', 'expires_at']) // ← 荷札を公開
            ->withTimestamps();                               // 荷札にも時刻をスタンプ
    }
}

withPivot() を忘れると Laravel は荷札を “無かったこと” にする

📦 倉庫に荷物を積む(追加カラム付き attach)

// 倉庫に積む:二重行を作らず、荷札だけ更新
$project->members()->syncWithoutDetaching([
    $memberId => [
        'role'       => 'Lead',          // 役割
        'granted_by' => $adminId,        // 誰が積んだ?
        'expires_at' => now()->addMonth()// いつまで?
    ]
]);

syncWithoutDetaching()=「既存を温存+足し算」安全ルート

🔍 荷札ごと取り出す(pivot プロパティ)

// 取り出し:荷札の中身も読める
$member = $project->members()->first();

$member->pivot->role;        // 'Lead'
$member->pivot->granted_by;  // 付与者 ID
$member->pivot->expires_at;  // 有効期限

▲ モデル同士の“線”を辿るだけで、倉庫のメタ情報も一緒に到着

📌 3つの持ち帰りポイント

  1. 荷札=追加カラムwithPivot() で“存在を宣言”する
  2. syncWithoutDetaching() で二重 INSERT を未然にブロック
  3. 複合主キー で物理的な重複をシャットアウト

これで“線路だけじゃ運べない情報”を pivot 倉庫に安全ストックできます。

👉 設計テンプレと実戦例はこちらwithPivot で追加カラム守る話

sync 系メソッド – 関係のリセットに注意

「sync は仲直りというより関係をぜんぶリセットして作り直す荒業。

フォームから空配列が飛んできた瞬間に

boot意図しないdetachが走り、ロールが蒸発──そんな地獄を何度も見てきました。

ここでは、消えないし重ならないを守る。 最短 3 パターンだけを貼っておきます。

よくやる事故→回避の実コード

// 💥 NG: チェックボックス空配列で全ロール蒸発
$user->roles()->sync($request->input('roles', []));

// 😊 OK: 空配列を門前払い(バリデーション+トランザクション)
$ids = $request->validate(['roles' => 'required|array|min:1'])['roles'];
DB::transaction(fn () => $user->roles()->sync($ids));

// 🙆‍♂️ 足し算だけしたいとき(既存は残す)
$user->roles()->syncWithoutDetaching([
    $roleId => ['granted_by' => Auth::id()],
]);

👉 5 大リスクと詳しい解説はこちらsync 系完全攻略

テストで怖さを食べる

「動くから OK」でデプロイすると 3 ヶ月後に墓場行き👻 二重行防止pivot カラム検証 だけは書こう。

it('syncWithoutDetaching は二重行を作らない', function () {
    $project = Project::factory()->create();
    $member  = Member::factory()->create();

    $project->members()->syncWithoutDetaching([$member->id => ['role' => 'Lead']]);
    $project->members()->syncWithoutDetaching([$member->id => ['role' => 'Lead']]);

    expect(DB::table('member_project')->count())->toBe(1);
});

👉 フルセットのサンプルは多対多リレーション運用大全

Collection でデータをちゃちゃっと後処理

配列操作の感覚で仕分けるのがCollectionの醍醐味。

取得後に「VIP だけ抽出→メールアドレスだけ欲しい」──そんなお願いを、 if 文も foreach も無しで片づけます。

$vipMails = User::with('roles')->get()
    ->filter(fn ($u) => $u->isVip())
    ->pluck('email');

👉️ Collectionの実践例で map() / chunk() / pipe() など “あと1手” を増やそう。→ Laravel Collection 実践パターン 7 選(Pest テスト付き)

Model が太ったら引っ越し

モデルがクエリも計算も通知もぜんぶ抱え込み始める状態になった責務分担を考えましょう。

たとえば「DB操作+計算+通知+エラー処理+テスト用のモック」が芋づる式に増え、 たった数メソッドの追加のつもりでもでも行数が雪だるま式に膨らんで一気に“読み切れない1,000 行ファイルへ化けかねません。

深掘りは記事は下記

👉️ アーキテクチャ、実は誰もわかってない説

👉️ 抽象講座

👉️ 超お手軽(してもいいのか?)DDD入門

Faker 活用ライトガイド — “それっぽい” ダミーデータを 5 秒で錬成する

「Seeder は書けるけど名前が全部 John Smith …?」
Faker で ロケール切替/Custom Provider/unique() × seed() をサッと仕込めば、
開発 DB が一瞬で “世界旅行 & 業界特化” のリア充データベースに早変わり。
デモもテストも 映え るサンプルが秒で量産できるので、
ダミーデータがダミーっぽすぎる 悩みはここで成仏させよう。

👉️ Faker活用ライトガイド – “それっぽい” ダミーデータを 5 秒で錬成する

FAQ

Eloquentで「N+1問題」って何?

リレーションを定義せず、ループ内で都度DBアクセスが発生し爆速で遅くなる現象。
with()load()で事前取得すると吉。

withPivot() 書かないと何が壊れる?

追加カラムが 取得も保存もスルー。null で帰ってきて静かに事故る。必ず宣言を。

attach() と sync() の決定的な違いは?

attach() は単発 INSERT、重複気にしない。sync() は「渡した ID 以外 DELETE→INSERT」のフル同期。誤爆率も桁違い。

syncWithoutDetaching() で 更新 はどうなる?

ID が既にあれば UPDATE、無ければ INSERT。削除はしない足し算専用メソッド。

複合主キー入れると何が嬉しい?

(user_id, role_id) の二重行を 物理的にブロック。コードの凡ミスでも DB が守ってくれる最終防壁。

モデルが太り始めた目安は?

モデルの、属性、関係、範囲、固有ルール以外のものが書かれた出した。または、これらが複雑化仕出した時。

Active Record と Data Mapper、結局どっち?

納期優先&単純 CRUD → Active Record(Eloquent)で爆速。長寿&複数 DB → Data Mapper(Doctrine)で後悔しない。

まとめと次の一歩

  • pivot = 関係の倉庫withPivot() と複合 PK で守る。
  • syncsyncWithoutDetaching() は目的で使い分け。
  • テストは未来の自分へのラブレター。最低 2 本でいいから書く。

気になる所はリンク先で理解を深めよう!


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください