Table of Contents
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
- Search – Look for a record matching the first array (where clause)
- Update – If found, update with the second array
- 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.
Leave a Reply