Inserting and Updating Records

save() inserts a new model or updates a loaded one, sending only the changed columns. Product carries #[ObservedBy(ProductObserver::class)]; the observer's saving() upper-cases the SKU, and its updated() logs price changes (the steps are in the figure at the start of this section):

app/Observers/ProductObserver.php, the updated methodPHP
public function updated(Product $product): void
{
    if ($product->wasChanged('price')) {
        $old = $product->getOriginal('price');
        Log::info("{$product->sku} price {$old} -> {$product->price}");
    }
}
save, dirty tracking, mass update, and updateOrCreateSQL
use App\Models\Product;
$p = new Product;
$p->fill(['sku' => 'bk-css-01', 'title' => 'CSS Layout', 'category_id' => 4, 'price' => 31]);
$p->save();
$p->price = 29.5;
echo json_encode([$p->isDirty('price'), $p->isDirty('title'), $p->getDirty()]), "\n";
$p->save();
Product::where('stock', 0)->update(['stock' => 5]);
Product::updateOrCreate(['sku' => 'BK-CSS-01'], ['title' => 'CSS Layout, 2nd ed.']);
Output
...
[true,false,{"price":29.5}]
-- update `products` set `price` = 29.5 where `id` = 10
-- update `products` set `stock` = 5 where `stock` = 0 and `products`.`deleted_at` is null
...
-- update `products` set `title` = 'CSS Layout, 2nd ed.' where `id` = 10
$ tail -n 1 storage/logs/laravel.log
[2026-09-23 08:18:55] local.INFO: BK-CSS-01 price 31.00 -> 29.50

The INSERT carried BK-CSS-01, and updated() still saw the old price because syncOriginal() runs after saved. The query-level update() changed rows it never loaded, so no event fired. Product::upsert($rows, uniqueBy: ['sku'], update: ['price']) with one existing and one new SKU became one insert ... on duplicate key update price = values(price), also without events; MySQL 524 ignores uniqueBy, matching on every unique index, and counted 3 rows (an update counts 2).