Model::updateOrCreate

Updates the first row matching the given attributes, or inserts one when nothing matches.

Signature
Model::updateOrCreate(array $attributes, array $values = []): static

Parameters

ParameterTypeDescription
$$attributesarrayColumn and value pairs identifying the row. Used as the where clause, and written to the new row when nothing matched. Required — there is no default.
$$valuesoptionalarray= []Attributes to write. Merged into the insert when creating, and filled onto the model and saved when a matching row was found.

Return value

staticThe model that was updated or created, already saved. wasRecentlyCreated distinguishes the two outcomes.

Errors

Throws Illuminate\Database\Eloquent\MassAssignmentException when a key in either array is not mass assignable and silent discarding is disabled.

updateOrCreate is firstOrCreate with a follow-up: it looks the row up by the first array, and if one already existed it fills the second array onto that model and saves it. If nothing matched, the two arrays are merged and inserted.

When to reach for it

Use it whenever a write should be idempotent on one logical key: syncing a record from a third-party API, importing a CSV that may be uploaded twice, recording the latest price for a route. Unlike firstOrCreate, it guarantees the stored row reflects the values you passed.

What trips people up

It is not a SQL upsert. Under the hood it runs a select, then either an insert or an update — two round trips, and no atomicity beyond what a unique index on the lookup columns gives you. For writing many rows at once, upsert() does it in one statement, though it skips model events and casts.

The first argument is required and has no default, so updateOrCreate([]) matches the whole table and updates whichever row happens to come back first. Keys in both arrays still go through mass assignment, so anything absent from $fillable is quietly dropped.

Example

Example · php
<?php

use App\Models\Flight;

// If a flight from Oakland to San Diego exists, its price and
// discounted columns are updated. Otherwise a new row is inserted
// with all four attributes.
$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

echo $flight->departure;                // Oakland
echo $flight->price;                    // 99
var_dump($flight->wasRecentlyCreated);  // bool(true) the first time, bool(false) after

When to use it

  • Sync a record from a third-party API keyed on its external id.
  • Make a CSV import safe to re-run without duplicating rows.
  • Store the latest price for a departure and destination pair.
  • Persist a user setting where the row may or may not exist yet.

Frequently asked questions

What is the difference between updateOrCreate and firstOrCreate?

updateOrCreate writes the second array in both cases; firstOrCreate only uses it when inserting and leaves an existing row untouched. Reach for updateOrCreate when the stored record must end up matching the values you passed.

Is updateOrCreate a real SQL upsert?

No. It runs a select and then an insert or an update, so it costs two queries and is not atomic on its own. Use upsert() for a single-statement batch write, and add a unique index on the lookup columns so concurrent calls cannot duplicate.

Does updateOrCreate fire model events?

Yes — the full saving, creating or updating, created or updated, and saved chain fires, so observers and attribute casts run as normal. upsert() is the method that skips them.

More examples

Branching on which outcome happened

wasRecentlyCreated tells you which path ran, and getChanges() reports which columns the update actually wrote. Note that updated_at appears in the changes: the save refreshes timestamps even when only one real column moved.

Example · php
<?php

use App\Models\Flight;

$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

if ($flight->wasRecentlyCreated) {
    echo "inserted\n";
} else {
    echo "updated\n";
    print_r($flight->getChanges());
}

/*
updated
Array
(
    [price] => 99
    [discounted] => 1
    [updated_at] => 2024-01-01 09:00:00
)
*/

Syncing many rows: updateOrCreate versus upsert

The loop costs two queries per row and fires saving, updating, created and friends, so observers and casts run normally. upsert() sends a single statement and is dramatically faster for large batches, at the cost of skipping model events. It also requires a primary or unique index on the columns named in uniqueBy.

Example · php
<?php

use App\Models\Flight;

$rows = [
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150],
];

// Two queries per row, but full model events and casts.
foreach ($rows as $row) {
    Flight::updateOrCreate(
        ['departure' => $row['departure'], 'destination' => $row['destination']],
        ['price' => $row['price']]
    );
}

// One statement for the whole batch. No model events fired.
Flight::upsert($rows, uniqueBy: ['departure', 'destination'], update: ['price']);

echo Flight::count();  // 2

Why the lookup columns need a unique index

Without the composite unique index, two requests arriving at the same moment both find no match and both insert, leaving duplicate flights that the next updateOrCreate will only half-fix. The index is what turns the select-then-write sequence into something the database can arbitrate.

Example · php
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('flights', function (Blueprint $table) {
    $table->unique(['departure', 'destination']);
});

// Now two concurrent workers cannot both insert: one wins,
// the loser's unique violation is caught and the winning row read back.
$flight = \App\Models\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99]
);

echo \App\Models\Flight::where('departure', 'Oakland')->count();  // 1

Discussion

  • Be the first to comment on this lesson.