8.

Laravel Eloquent モデル update 完全ガイド

編集
この記事の要点
  • 単一更新: $post = Post::find(1); $post->title = "new"; $post->save(); または $post->update(["title" => "new"])
  • 一括更新: Post::where("status", "draft")->update(["status" => "published"])
  • Mass Assignment 対策で $fillable または $guarded の設定が必須
  • モデル経由の update は updated_at が自動更新、クエリビルダ経由は手動
  • observer / event の updating / updated で履歴を残せる、トランザクションで整合性確保

4 つの update パターン

use App\Models\Post;

// 1. プロパティ代入 → save()
$post = Post::find(1);
$post->title = 'New Title';
$post->status = 'published';
$post->save();

// 2. モデルの update()(配列指定)
$post = Post::find(1);
$post->update(['title' => 'New Title', 'status' => 'published']);

// 3. クエリビルダ経由の一括 update
Post::where('status', 'draft')
    ->where('created_at', '<', now()->subDays(30))
    ->update(['status' => 'archived']);

// 4. updateOrCreate(存在すれば更新、無ければ作成)
Post::updateOrCreate(
    ['slug' => 'hello-world'],            // 検索条件
    ['title' => 'Hello', 'body' => '...'] // 値
);

save() vs update() の違い

メソッド引数fillable 制限イベント
$model->save()無し(プロパティ代入後)影響なしsaving/saved + updating/updated
$model->update([])配列★ fillable 制限ありsaving/saved + updating/updated
Model::where()->update([])配列制限なし(クエリビルダ)★ イベント発火しない

fillable / guarded(Mass Assignment 対策)

配列から一括代入できる属性を制限することで、悪意あるリクエストで is_admin = true 等を勝手に上書きされる事故を防ぎます:

class Post extends Model
{
    // ホワイトリスト方式(推奨)
    protected $fillable = ['title', 'body', 'status'];

    // または全許可(非推奨。$guarded で例外指定)
    protected $guarded = ['id', 'is_admin'];

    // 全許可(テスト/Seeder 用、本番禁止)
    protected $guarded = [];
}

// fillable に無いキーは update() でスキップされる
$post->update([
    'title' => 'OK',
    'is_admin' => true,   // ← 無視される(fillable に無いため)
]);

// 強制的に上書き
$post->forceFill(['is_admin' => true])->save();

updated_at の自動更新

// デフォルトで created_at / updated_at は自動管理
class Post extends Model
{
    public $timestamps = true;  // デフォルト
}

// 自動更新を止める
class Log extends Model
{
    public $timestamps = false;
}

// 一時的に updated_at を更新しない(修正リバースで便利)
$post->timestamps = false;
$post->update(['title' => 'Quiet fix']);
$post->timestamps = true;

// updateQuietly()(Laravel 8+)でイベント発火 + updated_at 更新を制御
$post->updateQuietly(['title' => 'No event']);
// → イベント発火しない、updated_at は更新する

クエリビルダ経由の一括 update の特性

// ✅ 速い: 1 SQL で全件更新
Post::where('status', 'draft')->update(['status' => 'published']);
// UPDATE posts SET status = 'published' WHERE status = 'draft'

// ⚠️ 注意点:
//   - モデルイベント (updating/updated) は発火しない
//   - observer も呼ばれない
//   - updated_at は自動更新される(カスタムタイムスタンプは効く)
//   - Mutator (setXxxAttribute) は通らない(DB 直アクセス)

// ✅ イベントを起こしたいなら chunk で個別 save
Post::where('status', 'draft')
    ->chunkById(100, function ($posts) {
        foreach ($posts as $post) {
            $post->update(['status' => 'published']);
        }
    });

increment / decrement

// インスタンス経由
$post->increment('view_count');           // +1
$post->increment('view_count', 5);        // +5
$post->decrement('stock', 1);             // -1

// クエリビルダ経由
Post::where('id', 1)->increment('view_count');

// 同時に他カラムも更新(原子的)
$post->increment('view_count', 1, ['last_viewed_at' => now()]);

updateOrCreate / firstOrCreate / firstOrNew

// 既存なら更新、無ければ新規(upsert)
$post = Post::updateOrCreate(
    ['slug' => 'hello'],        // 検索条件(複数キー可)
    ['title' => 'Hello', 'body' => '...']
);

// 既存なら取得、無ければ作成(更新しない)
$post = Post::firstOrCreate(
    ['slug' => 'hello'],
    ['title' => 'Hello', 'body' => '...']
);

// 既存なら取得、無ければ未保存インスタンス(save 必要)
$post = Post::firstOrNew(['slug' => 'hello']);
$post->title = 'Hello';
$post->save();

// 複数件の upsert(Laravel 8+)
Post::upsert(
    [
        ['slug' => 'a', 'title' => 'A'],
        ['slug' => 'b', 'title' => 'B'],
    ],
    uniqueBy: ['slug'],     // 一意キー
    update: ['title']       // 競合時に更新するカラム
);

イベント / Observer で履歴を取る

// app/Observers/PostObserver.php
class PostObserver
{
    public function updating(Post $post): void
    {
        // save 前。getDirty() で変更されるカラム一覧を取れる
        if ($post->isDirty('status')) {
            PostStatusLog::create([
                'post_id'    => $post->id,
                'from'       => $post->getOriginal('status'),
                'to'         => $post->status,
                'changed_by' => auth()->id(),
            ]);
        }
    }

    public function updated(Post $post): void
    {
        // save 後
        if ($post->wasChanged('title')) {
            Cache::forget("post_html_{$post->id}");
        }
    }
}

// app/Providers/AppServiceProvider.php boot()
Post::observe(PostObserver::class);

トランザクションで整合性を担保

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($postId) {
    $post = Post::lockForUpdate()->find($postId);  // 行ロック

    $post->update(['status' => 'published']);
    $post->author->increment('published_count');

    Notification::send($post->subscribers, new PostPublished($post));
});

// 手動トランザクション
DB::beginTransaction();
try {
    // ... 複数 update ...
    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

楽観的ロック

同時編集による上書き事故を防ぐパターン。version カラムで判定:

// posts に version カラムを追加(int default 0)

$post = Post::find($id);
$currentVersion = $post->version;

// 更新時に version 一致を条件にする
$updated = Post::where('id', $id)
    ->where('version', $currentVersion)
    ->update([
        'title' => $request->title,
        'version' => $currentVersion + 1,
    ]);

if ($updated === 0) {
    // 他のユーザが先に更新した
    abort(409, '他のユーザによって編集されました。再読み込みしてください');
}

FAQ

Q: save() しても DB が更新されない
A: 変更前と同じ値の場合、Eloquent は SQL を発行しません。$model->isDirty()getDirty() で確認できます。

Q: Mass Assignment 例外(MassAssignmentException)が出る
A: $fillable に対象カラムを追加するか、forceFill() を使ってください。

Q: 一括更新で観測者を呼びたい
A: クエリビルダ経由は observer をスキップします。chunkById でループしながら個別 save() するか、SQL 直書き後に手動で発火させてください。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. モデルの作成
  2. $fillable $guarded $hiddenの説明
  3. テーブルの紐づけ
  4. 主キーの指定とインクリメント
  5. タイムスタンプ
  6. モデルでselect
  7. モデルでinsert
  8. モデルでupdate
  9. 現在値に加算する方法
  10. created_at/updated_atの別名指定

最近更新/作成されたページ