Saving Related Models

Attaching, Syncing, and Saving Related Models

Writing through a relationship fills in the keys. On hasMany and morphMany, save(), saveMany(), create() and createMany() set the foreign key (and type) before inserting; on belongsTo, associate() and dissociate() set or clear the child's key without saving. Pivot rows are managed by attach(), detach(), sync(), syncWithoutDetaching(), toggle() and updateExistingPivot(), which accept IDs, models, or id => [pivot columns] arrays.

A review saved through two relationships, then an order's lines rewrittenSQL
use App\Models\{Customer, Order, Product, Review};
$review = (new Review)->forceFill(['rating' => 4]);
$review->customer()->associate(Customer::find(8));
Product::find(5)->reviews()->save($review);
$order = Order::find(9);                        // holds products 4 and 8
$order->products()->attach([1 => ['quantity' => 1]]);
echo json_encode($order->products()->sync([1 => ['quantity' => 3], 4])), "\n";
echo json_encode($order->products()->toggle([4, 7 => ['quantity' => 1]])), "\n";
$gone = $order->products()->detach(1);
echo "$gone detached, left: {$order->products()->pluck('sku')}\n";
Output
...
-- insert into `reviews` (`rating`, `customer_id`, `product_id`, `created_at`) values (4, 8, 5,
    '2026-09-23 08:22:39')
...
-- select * from `order_items` where `order_items`.`order_id` = 9
-- select * from `order_items` where `order_items`.`order_id` = 9 and
    `order_items`.`product_id` in (8)
-- delete from `order_items` where (`order_id` = 9 and `product_id` = 8)
-- select * from `order_items` where `order_items`.`order_id` = 9 and
    `order_items`.`product_id` in (1)
-- update `order_items` set `quantity` = 3 where `order_id` = 9 and `product_id` = 1
{"attached":[],"detached":{"2":8},"updated":[1]}
...
{"attached":[7],"detached":[4]}
...
1 detached, left: ["AC-MUG-01"]

One INSERT stored the review with both keys. sync() made the pivot match the list: 8 went, 1 was updated, 4 was untouched. toggle() removed 4 and added 7. With a custom pivot each detach selects and deletes row by row so events fire; a plain pivot sends one DELETE. Wrap such rewrites in DB::transaction() (Transactions and Locks).