$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[September 23, 2026] Eloquent ORM

Eloquent Soft Deletes

Eloquent Soft Deletes: Tips & Tricks

────────────────────────────────────────────────────────

Eloquent Soft Deletes: Tips & Tricks

Tip: Unique Constraints Break with Soft Deletes

A soft-deleted record still exists in the database. If you have a unique constraint on slug, you can't create a new record with the same slug.

Fix: Use a composite unique index:

$table->unique(['slug', 'deleted_at']);

Gotcha: withTrashed() Affects Relationships Too

$user->posts()->withTrashed()->get();

Without withTrashed(), soft-deleted related models are excluded.

Tip: Restore with Relations

$post->restore();
$post->comments()->withTrashed()->each(fn($c) => $c->restore());

Gotcha: forceDelete() is Permanent

$post->forceDelete(); // Gone forever

No undo. Be careful.

Tip: Check if Soft Deleted

$post->trashed(); // true if soft deleted
$post->restore(); // Undelete

Tip: Auto-Delete Relations on Soft Delete

protected static function booted(): void
{
    static::deleting(function ($post) {
        if (! $post->isForceDeleting()) {
            $post->comments()->delete(); // Soft deletes comments too
        }
    });
}

Gotcha: onlyTrashed() Returns Only Deleted

Post::onlyTrashed()->get(); // Only soft-deleted posts
Post::withTrashed()->get(); // All posts including deleted

Tip: Schedule Cleanup of Old Soft Deletes

$schedule->call(function () {
    Post::onlyTrashed()->where('deleted_at', '<', now()->subDays(90))->forceDelete();
})->daily();

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Eloquent is Laravel's most controversial feature — developers either love its convenience or hate its magic. My take after years of using both Eloquent and raw SQL: Eloquent is excellent for 80% of queries and wrong for the other 20%. The skill is knowing which is which. If a query involves more than three joins, window functions, or complex subqueries, reach for the Query Builder or raw SQL. Eloquent's beauty is in its simplicity for CRUD; fighting it for complex queries helps no one.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts