Soft Deletes in Eloquent
In this final article of our Database & Eloquent category, we'll walk through how to implement soft deletes, query soft-deleted records, and restore or permanently remove them when needed.
Introduction
Deleting data is often riskier than it seems. A user accidentally deletes an important post, an admin removes the wrong record, or you simply want to keep a history of what used to exist for auditing purposes. Once a row is truly gone from the database, recovering it usually means restoring from a backup — a slow, heavy-handed solution for what might just be a single accidental click.
Laravel's Eloquent offers an elegant alternative: soft deletes. Instead of permanently removing a record from the database, a soft-deleted record is simply marked as deleted using a timestamp column, while the actual row remains intact. Your application then automatically excludes soft-deleted records from normal queries, making them behave as if they were gone — until you decide to restore them.
In this final article of our Database & Eloquent category, we'll walk through how to implement soft deletes, query soft-deleted records, and restore or permanently remove them when needed.
What Are Soft Deletes?
When a model uses soft deletes, calling ->delete() on it doesn't actually run a SQL DELETE statement. Instead, it sets a deleted_at timestamp column on that row. From that point on, Eloquent automatically filters out any record with a non-null deleted_at value from your normal queries, effectively hiding it without truly removing it from the database.
Setting Up Soft Deletes
Step 1: Add the deleted_at Column via Migration
php
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes();
});If you're creating a new table, you can include this directly:
php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->softDeletes();
$table->timestamps();
});softDeletes() is a convenient shortcut that adds a nullable deleted_at timestamp column to the table.
Step 2: Add the SoftDeletes Trait to the Model
php
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}That's it — with the column in place and the trait added, your model now supports soft deletes automatically.
Using Soft Deletes
Deleting a Record
php
$post = Post::find(1);
$post->delete();This looks identical to a normal delete, but behind the scenes, it only sets the deleted_at column instead of removing the row.
Querying Normally
php
$posts = Post::all();Soft-deleted posts are automatically excluded from this query — Eloquent adds a WHERE deleted_at IS NULL condition behind the scenes without you needing to write it yourself.
Including Soft-Deleted Records
If you specifically need to include soft-deleted records alongside normal ones:
php
$posts = Post::withTrashed()->get();Querying Only Soft-Deleted Records
php
$deletedPosts = Post::onlyTrashed()->get();Restoring Soft-Deleted Records
Since the data is still physically present in the database, restoring a soft-deleted record is simple:
php
$post = Post::onlyTrashed()->find(1);
$post->restore();This clears the deleted_at column, making the record visible in normal queries again, exactly as if it had never been deleted.
Permanently Deleting a Record
If you truly want to remove a soft-deleted record from the database entirely, use forceDelete():
php
$post = Post::onlyTrashed()->find(1);
$post->forceDelete();Unlike a regular delete() call on a soft-deleting model, forceDelete() genuinely removes the row from the database, with no way to recover it afterward.
Checking If a Record Is Soft-Deleted
php
if ($post->trashed()) {
echo 'This post has been deleted.';
}The trashed() method returns true if the model's deleted_at column is set, giving you a clean way to check its state within your application logic.
Handling Relationships with Soft Deletes
Soft deletes also affect relationships. By default, when you query related records (like a user's posts), soft-deleted posts are excluded, just like any other query. If you need to include them:
php
$user->posts()->withTrashed()->get();This is worth keeping in mind, especially with route model binding (covered earlier in this series) — by default, implicit binding won't find a soft-deleted record unless you explicitly allow it:
php
Route::get('/posts/{post}', [PostController::class, 'show'])->withTrashed();Tips for Working with Soft Deletes
Use soft deletes for any data where accidental deletion would be costly — user accounts, orders, published content — rather than for trivial, easily re-creatable data.
Remember
delete()behaves differently onceSoftDeletesis added — it no longer permanently removes the row, which can be surprising if you're not expecting it.Use
forceDelete()deliberately and sparingly, since it's the only way to truly, permanently remove a soft-deleted record.Consider adding an admin interface for restoring trashed records, since soft deletes are most useful when there's an easy way to actually recover them when needed.
Be mindful of unique constraints in your database — since soft-deleted rows still exist, a unique column (like
email) can still trigger conflicts even after a record is "deleted," unless you account for this in your validation logic.
FAQ About Soft Deletes
1. Does adding SoftDeletes change how normal queries behave? Yes — once the trait is added, all standard Eloquent queries automatically exclude soft-deleted records, without you needing to add any extra conditions yourself.
2. What happens if I forget to add the deleted_at column but still add the SoftDeletes trait? You'll get a database error when trying to delete a record, since Eloquent expects that column to exist in order to perform a soft delete.
3. Is there a way to permanently delete a record without first soft-deleting it? Yes, calling forceDelete() directly (without calling delete() first) immediately and permanently removes the record, skipping the soft-delete step entirely.
4. Do soft-deleted records still count toward database storage and backups? Yes, since the rows still physically exist in the database, they continue to take up storage space and are included in backups, unlike truly deleted data.
5. Can I combine soft deletes with scheduled permanent deletion, like removing anything trashed for more than 30 days? Yes, this is a common pattern, often implemented using Laravel's task scheduling feature to periodically run a job that calls forceDelete() on old, soft-deleted records.
6. Does find() return soft-deleted records by default? No, by default find() (and other standard queries) only returns non-deleted records. You need withTrashed() or onlyTrashed() explicitly to include soft-deleted ones.
Conclusion
Soft deletes offer a safer, more forgiving approach to handling deletions, letting you "remove" records from normal view while preserving the ability to recover, audit, or investigate them later. For any data where an accidental deletion could be costly, this small addition to your models provides valuable peace of mind.
With this article, we've now completed the full Database & Eloquent ORM category — from configuring your MySQL connection, through migrations, seeders and factories, Eloquent basics, the Query Builder, relationships, eager loading, attribute customization, and finally soft deletes. You now have a comprehensive foundation for storing, retrieving, and managing data in any Laravel application.
Found this helpful? Share it!