MeshWorld India LogoMeshWorld.

Move Records Between Tables in Laravel

(Updated: Mar 27, 2026)
Listen to ArticleAI Speech
~6 min read narration
100%
Move Records Between Tables in Laravel

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.

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_orders table and a completed_orders archive 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:

PHP
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:

PHP
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.

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:

PHP
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:

PHP
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.

Reader Quality Feedback

Did this technical guide help solve your problem?

Suggest Errata ($0)
Vishnu
Primary Author

Vishnu

Founder & Principal Architect at MeshWorld. Senior engineer and instructor specializing in AI agent systems, scalable web architecture, and modern development workflows.

Explore Author Archive
Compute Fuel & Open Testbed
100% Independent & Verified

Fuel High-Density, Zero-Fluff Engineering Deep-Dives

Every guide on MeshWorld is validated on physical Linux nodes and reproducible testbeds. If this article saved you hours of debugging or unblocked production, consider funding our next cluster run.

Weekly Dispatch

Join MeshWorld Dispatch

Get practical tutorials, system blueprints, and curated AI engineering notes straight to your inbox. No fluff, zero spam.

Zero spam. 1-click unsubscribe anytime.Prefer RSS?
Curated Continuations

Up Next in This Domain.

Browse Full Archive