$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[August 24, 2026] Eloquent ORM

Eloquent Upsert: Bulk Insert or Update

Eloquent Upsert: Bulk Insert or Update

────────────────────────────────────────────────────────

Eloquent Upsert: Bulk Insert or Update

Tip: upsert() for Bulk Operations

Post::upsert([
    ['slug' => 'post-1', 'title' => 'First'],
    ['slug' => 'post-2', 'title' => 'Second'],
], uniqueBy: ['slug'], update: ['title']);

Inserts new records, updates existing ones. One query.

Gotcha: upsert() Needs Unique Index

The uniqueBy columns must have a unique index in the database, or it will fail.

Tip: updateOrCreate() for Single Records

Post::updateOrCreate(
    ['slug' => $slug],
    ['title' => $title, 'content' => $content]
);

Finds by slug, creates if not found, updates if found.

Gotcha: updateOrCreate() is Two Queries

It does a SELECT first, then INSERT or UPDATE. Not atomic. Use upsert() for concurrency safety.

Tip: firstOrCreate() with Defaults

$user = User::firstOrCreate(
    ['email' => 'john@example.com'],
    ['name' => 'John', 'role' => 'user']
);

Second argument provides defaults for new records.

Gotcha: upsert() Doesn't Fire Model Events

No creating, created, updating, updated events fire during upsert().

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Upsert operations are a lifesaver for syncing data, but Eloquent's upsert() has a limitation: it doesn't fire model events. The inserted or updated records bypass creating, saved, and all booted trait logic. I've seen teams rely on upsert() for bulk operations and wonder why their observers didn't fire. For any upsert operation that needs event handling, fall back to individual updateOrCreate() calls with chunking.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts