Reader Stacks

How to Fetch (and Restore) Soft-Deleted Records in Laravel

withTrashed(), onlyTrashed(), and restore() — plus the query-scope gotcha that makes soft-deleted rows silently vanish from relationships by default.

How to Fetch (and Restore) Soft-Deleted Records in Laravel

Soft deletes (the SoftDeletes trait) don't actually remove a row — they set a deleted_at timestamp and Eloquent silently excludes it from normal queries. That silence is the whole feature, and also the thing people trip over.

Including soft-deleted rows: withTrashed()

$allUsers = User::withTrashed()->get(); // includes soft-deleted rows

Only the soft-deleted rows: onlyTrashed()

$deletedUsers = User::onlyTrashed()->get();

Restoring a soft-deleted row

$user = User::onlyTrashed()->find($id);
$user->restore();

Or in one line:

User::onlyTrashed()->where('id', $id)->restore();

Permanently deleting it

User::onlyTrashed()->where('id', $id)->forceDelete();

The relationship gotcha

This is the part that causes real bugs: a soft-deleted parent's related rows don't show up through a normal relationship query either, because the relationship query goes through the same SoftDeletes global scope.

// A post's soft-deleted comments are invisible here by default:
$post->comments;

// Include them explicitly:
$post->comments()->withTrashed()->get();

This matters most for withCount() and eager-loading: a count that silently excludes soft-deleted related rows can look like a data bug when it's actually the intended behavior — just not the behavior you expected in that specific query.

Route-model binding also respects the soft-delete scope

A route such as /users/{user} normally will not resolve a soft-deleted User, even though the row still exists. That is usually desirable for public routes. Administrative "trash" screens need to opt into trashed models deliberately rather than assuming route-model binding behaves differently from an ordinary Eloquent query.

Restoring a parent does not restore its children automatically

If deleting a parent also soft-deletes related rows through model events or application code, $parent->restore() only restores that parent unless you explicitly implement restoration for the related models. The same asymmetry applies to permanent deletion. Treat cascading soft-delete behavior as application policy and test it; a database foreign-key cascade only applies to actual deletes, not to setting deleted_at.

Events are different for delete(), restore(), and forceDelete()

Soft-deleting and restoring models fire lifecycle events that observers can react to. A permanent delete follows the force-delete path instead. This matters if search indexes, audit logs, files, or external systems are synchronized from model events — restoring a row may need to republish state that was removed when the model was soft-deleted.

Use prunable retention for data that should not stay in trash forever

Soft delete is often a temporary recovery window rather than indefinite storage. If policy says deleted records should be purged after a retention period, make that an explicit scheduled cleanup rather than relying on an administrator to empty trash manually. The key is to distinguish "recoverable deletion" from "permanent archival"; soft deletes are convenient for the first, but they are not a substitute for a real audit/history model when historical records must be retained permanently.

Check for null before calling restore()

find($id) can still return null, even inside onlyTrashed(). For controller code, use findOrFail() when a missing trashed record should become a 404, or handle the null case explicitly before calling restore(). Otherwise the restore path turns a normal "record not found" condition into a method call on null.

Topics: Database Queries & Eloquent