Laravel has no single “move record” method, but it does have replicate() — an Eloquent method that clones a model instance without persisting it. Combined with setTable() and delete(), you get a clean three-step pattern: copy the record to the target table, confirm it saved, then delete the original. Wrapping the whole thing in DB::transaction() ensures you never end up with a record in both tables or neither.
:::note[TL;DR]
$model->replicate()creates a non-persisted clone of an Eloquent model$clone->setTable('target_table')redirects the save to a different table$clone->save()persists the clone;$original->delete()removes the sourceDB::transaction()makes the move atomic — if the insert fails, the delete doesn’t happen- Works in Laravel 8 through 12 :::
Why does moving records need a transaction?
Without a transaction, the two operations are independent. If save() succeeds but delete() fails (or the reverse), your data ends up in an inconsistent state — duplicated or lost.
The scenario: Your app has an
active_orderstable and acompleted_ordersarchive table. When a customer’s order is fulfilled, you move it. If your server dies between the insert and the delete, you’d have a completed order still showing as active. That causes a support ticket at minimum, a double shipment at worst.
DB::transaction() handles this. If anything inside the closure throws an exception, the entire block is rolled back.
How do you move a single record with replicate() and setTable()?
First, fetch the record from the source table. Then clone it, redirect it to the target table, save it, and delete the original. Wrap all three write operations in a transaction:
use Illuminate\Support\Facades\DB;
use App\Models\ActiveOrder;
DB::transaction(function () use ($orderId) {
// Fetch the record to move
$order = ActiveOrder::findOrFail($orderId);
// Clone it and point the clone at the archive table
$archived = $order->replicate();
$archived->setTable('completed_orders');
$archived->save();
// Delete the original only after the insert succeeds
$order->delete();
});
replicate() copies all column values into a new model instance. setTable() overrides the table name for that instance only — the ActiveOrder model’s default table stays active_orders. The original model is unaffected.
If $archived->save() throws (connection issue, constraint violation, whatever), the transaction rolls back and the original record stays put.
How do you move multiple records at once?
Use each() to iterate, keeping the transaction around the whole batch so it’s all-or-nothing:
use Illuminate\Support\Facades\DB;
use App\Models\User;
DB::transaction(function () {
User::query()
->where('last_login', '<', now()->subYears(3))
->each(function ($user) {
$archived = $user->replicate();
$archived->setTable('inactive_users');
$archived->save();
$user->delete();
});
});
This moves all users inactive for over three years from users to inactive_users in a single transaction. If any record fails mid-batch, the entire batch rolls back — no partial moves.
:::warning
each() loads records in chunks (default: 1000). For very large tables, the transaction may hold open long enough to cause lock contention. For bulk archiving jobs, consider chunking explicitly with chunk() and running each chunk in its own transaction, accepting that a failure won’t roll back already-committed chunks.
:::
Can you chain replicate() and setTable() together?
Yes. The method chaining is clean for the save step, though you still need to hold a reference to the original for the delete:
DB::transaction(function () use ($order) {
$order->replicate()
->setTable('completed_orders')
->save();
$order->delete();
});
Identical result. The chained version is fine when the code is already clear about what $order refers to.
The scenario: You’re writing an order fulfilment job that runs every hour. The job marks orders as complete and archives them. Chaining keeps each move operation to three lines, which means the loop body stays scannable even when someone reads it at 9 AM after a deploy.
What if the two tables have different columns?
replicate() copies all attributes from the source model. If the target table has columns that don’t exist on the source, you’ll need to set them manually before saving:
DB::transaction(function () use ($order) {
$archived = $order->replicate();
$archived->setTable('completed_orders');
$archived->archived_at = now(); // column that only exists on completed_orders
$archived->save();
$order->delete();
});
If the source has columns that don’t exist on the target, you’ll get a database error on save(). In that case, unset the extra attributes: unset($archived->some_column) before saving.
Summary
replicate()clones a model in memory without saving it.setTable()redirects where it saves.- Always wrap the insert + delete in
DB::transaction(). Partial moves corrupt your data. - For bulk moves,
each()inside a single transaction handles the whole batch atomically — but watch for lock contention on large tables. - Mismatched columns between tables need manual attribute adjustments before
save(). - This pattern works in Laravel 8 through 12.
FAQ
Does replicate() copy the primary key?
No. replicate() excludes the primary key by default, so the new record gets a fresh auto-increment ID. You can pass an array of attributes to exclude additional fields: $model->replicate(['created_at', 'some_column']).
Does this work with soft-deleted models?
Yes, but be careful. If the source model uses SoftDeletes, replicate() will copy the deleted_at value too. Explicitly set $archived->deleted_at = null if the archive table also uses soft deletes and you want the record to appear as active there.
What’s the difference between replicate() and a raw INSERT INTO … SELECT?
replicate() goes through Eloquent — it fires model events (creating, created, etc.) and respects casts and mutators. A raw query is faster for bulk operations but bypasses all of that. Use replicate() when model events matter; use raw SQL when you’re moving thousands of rows in a maintenance job.
Can I move records without using replicate()?
Yes. You can use DB::table('target')->insert($sourceRecord->toArray()) followed by $sourceRecord->delete(), all inside a transaction. It’s more verbose but doesn’t require a model class for the target table.
Does setTable() persist after the operation?
Only for that model instance. The class default table is unchanged. setTable() sets the table on the specific object you called it on.
What to Read Next
- Get Last 30 Days Records in Laravel — query pattern for identifying records to archive by date range.
- firstOrCreate Model Method in Laravel — complementary Eloquent pattern for inserting only when a record doesn’t already exist.
- Create Indexes with Migration in Laravel — if your archive table needs indexes to stay fast as it grows.