Idempotent Database Operations with updateOrCreate

📖 3 minutes read





Idempotent Database Operations with updateOrCreate

Idempotent Database Operations with updateOrCreate

When syncing data from external APIs or running scheduled jobs, you need operations that are safe to run multiple times without side effects. This is called idempotency.

Laravel’s updateOrCreate() method makes database operations naturally idempotent by either updating existing records or creating new ones based on match conditions.

The Problem: Duplicate Records

// ❌ Bad: Creates duplicates every time
public function syncFromApi()
{
    $items = Http::get('/api/products')->json();
    
    foreach ($items as $item) {
        Product::create([
            'external_id' => $item['id'],
            'name' => $item['name'],
            'price' => $item['price'],
        ]);
    }
}

Run this twice, and you’ll have two copies of every product. Schedule it to run hourly, and you’re accumulating garbage data.

The Solution: updateOrCreate()

// ✅ Good: Safe to run multiple times
public function syncFromApi()
{
    $items = Http::get('/api/products')->json();
    
    foreach ($items as $item) {
        Product::updateOrCreate(
            [
                'external_id' => $item['id'], // Match on this
            ],
            [
                'name' => $item['name'],       // Update these fields
                'price' => $item['price'],
            ]
        );
    }
}

Now you can run this sync as many times as needed. Existing products update, new products create. No duplicates, no manual cleanup.

How It Works

  1. Search – Look for a record matching the first array (where clause)
  2. Update – If found, update with the second array
  3. Create – If not found, merge both arrays and insert

Think of the first array as your “unique identifier” and the second as your “payload to save.”

Common Patterns

Composite Keys

// Match on multiple fields
ProductVariant::updateOrCreate(
    [
        'product_id' => $productId,
        'variant_code' => $variantCode,
    ],
    [
        'name' => $name,
        'stock' => $stock,
        'price' => $price,
    ]
);

Webhook Processing

// Idempotent webhook handler
public function handle(array $webhook)
{
    Order::updateOrCreate(
        ['external_order_id' => $webhook['order_id']],
        [
            'status' => $webhook['status'],
            'total' => $webhook['total'],
            'customer_email' => $webhook['email'],
        ]
    );
}

If the webhook fires twice (it happens!), you get the same result. No duplicate orders.

Scheduled Imports

// Hourly cron job - always safe
$items = Cache::remember('inventory_feed', 3600, fn() => 
    Http::get($inventoryUrl)->json()
);

foreach ($items as $item) {
    InventoryItem::updateOrCreate(
        ['sku' => $item['sku']],
        [
            'quantity' => $item['qty'],
            'warehouse' => $item['location'],
            'synced_at' => now(),
        ]
    );
}

Performance Considerations

updateOrCreate() runs one query to check + one query to update/insert. For bulk operations with thousands of records, consider using upsert() instead:

// Bulk upsert (Laravel 8+)
Product::upsert(
    [
        ['external_id' => 1, 'name' => 'Widget', 'price' => 10.00],
        ['external_id' => 2, 'name' => 'Gadget', 'price' => 20.00],
        // ... thousands more
    ],
    ['external_id'], // Unique by
    ['name', 'price'] // Update these
);

This runs a single bulk INSERT … ON DUPLICATE KEY UPDATE query, dramatically faster for large datasets.

When to Use updateOrCreate()

  • API syncing – External data refreshes
  • Webhook handlers – Idempotent event processing
  • Scheduled jobs – Cron tasks that might retry
  • Import scripts – CSV/Excel uploads
  • Cache warming – Rebuild cached aggregates

Key Takeaway

Idempotency isn’t just a nice-to-have—it’s essential for reliable systems. Any operation that syncs external data or runs on a schedule should be idempotent by default. updateOrCreate() makes this trivial in Laravel.

The alternative is manually checking if records exist before deciding whether to create or update. That’s error-prone, verbose, and creates race conditions. Let Eloquent handle it.


Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *