Hello! Welcome to the next lesson in our Advanced Eloquent module.
In the last lesson, we mastered constrained and nested eager loading, giving you precise control over fetching complex relationship data efficiently. We focused on optimizing how we read data. Today, we shift our focus to the data lifecycle itself, specifically what happens when data is "removed".
In many applications, permanently deleting data is risky. You might lose important history, break audit trails, or be unable to recover from an accidental deletion. This is where soft deletes come in.
This lesson covers how to implement soft deletes to manage non-destructive record removal. You will learn how to mark records as "deleted" without actually removing them from your database, how to query them, and how to restore or permanently delete them when needed. This is a fundamental pattern for building robust, data-resilient applications.
1. What Are Soft Deletes and Why Use Them?
A "hard delete" refers to a standard SQL DELETE statement, which permanently removes a row from a database table. Once it's gone, it's gone (barring database backups).
Soft Deletion is a different approach. Instead of deleting the row, you simply mark it as deleted. In Laravel, this is done by convention with a deleted_at timestamp column.
- If
deleted_atisNULL, the record is considered active. - If
deleted_atcontains a timestamp, the record is considered "soft deleted" or "trashed".
Your application, through Eloquent, is then configured to automatically ignore these soft-deleted records in all standard queries, making them seem as if they've been deleted.
To Delete or to Soft Delete - Sudhakar's blog
To understand the key use cases for soft deletes, please read the introductory sections of this blog post.
Read the sections titled "To Delete or to Soft Delete, That is the Question!" and "When would it make sense to use Soft Delete?". This will clarify the business reasons for choosing this pattern over permanent deletion.
As the post highlights, the main benefits are:
- Data Recovery: Easily "undelete" records.
- Audit Trails & History: Retain a full history of all data, including what was deleted and when, which is often a legal or business requirement.
- Referential Integrity: Avoid cascading deletes that could wipe out related data. A soft-deleted
userrecord can still be referenced by historicalorderrecords.
2. Implementing Soft Deletes in Laravel
Enabling soft deletes in Laravel is a straightforward, two-step process: you need to update your database table and then your Eloquent model.
This video provides a clear, step-by-step walkthrough of the entire process, from a fresh start to a fully functional implementation. We will be referencing it throughout the lesson.
How to use soft deletes in Laravel
Let's start with this excellent tutorial from Andrew Schmelyun, which will guide us through the core implementation.
Watch from the beginning to 03:08. The video will first explain the concept and then walk you through adding the necessary database migration and model trait.
Let's break down the two key steps shown in the video.
Step 1: The Migration
You need a nullable deleted_at timestamp column in your table. Laravel's Schema builder provides a convenient helper method for this.
-
Create a migration file to alter your existing table (or add it to a new table creation).
php artisan make:migration add_soft_deletes_to_posts_table --table=posts -
Use the
softDeletes()method in your migration'sup()function. In thedown()function, you should use the correspondingdropSoftDeletes()method to make the migration reversible.// In the 'up()' method of your migration public function up(): void { Schema::table('posts', function (Blueprint $table) { $table->softDeletes(); // Adds a nullable 'deleted_at' timestamp column }); } // In the 'down()' method public function down(): void { Schema::table('posts', function (Blueprint $table) { $table->dropSoftDeletes(); // Removes the 'deleted_at' column }); } -
Run the migration:
php artisan migrate
After running the migration, your table will have the required deleted_at column, ready to store timestamps.

Step 2: The Model Trait
Next, you must tell your Eloquent model to use the soft delete functionality. This is done by adding the SoftDeletes trait to your model class.
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; // 1. Import the trait
class Post extends Model
{
use HasFactory, SoftDeletes; // 2. Use the trait in the model
// ... rest of your model
}
That's it! With the migration and the trait in place, your Post model is now configured for soft deletes. Calling $post->delete() will now perform a soft delete instead of a hard delete.
3. Core Operations: Delete, Restore, and Query
Now that soft deletes are enabled, let's explore how to interact with your data.
Deleting and Restoring Records
- Soft Delete:
delete()
When you call thedelete()method on a model instance, Eloquent will set thedeleted_atcolumn to the current timestamp and save the model.$post = Post::find(1); $post->delete(); // Sets 'deleted_at' and saves. - Restore:
restore()
To "undelete" a record, you can call therestore()method. This will set thedeleted_atcolumn back toNULL.// First, you must find the trashed post $post = Post::withTrashed()->find(1); $post->restore(); // Sets 'deleted_at' to NULL. - Permanent Delete:
forceDelete()
If you need to permanently remove a record from the database, use theforceDelete()method. This performs a hard delete and cannot be undone.$post = Post::withTrashed()->find(1); $post->forceDelete(); // The row is now permanently gone.
Querying Soft-Deleted Data
By default, Eloquent automatically excludes soft-deleted records from all query results.

To control this behavior, Eloquent provides several helpful methods:
withTrashed(): Includes soft-deleted records in the results.// Get all posts, including those in the trash $allPosts = Post::withTrashed()->get();onlyTrashed(): Retrieves only the soft-deleted records.// Get only the posts that are in the trash $trashedPosts = Post::onlyTrashed()->get();trashed(): Checks if a specific model instance has been soft-deleted.$post = Post::withTrashed()->find(1); if ($post->trashed()) { // This post is in the trash. }
The following video segments demonstrate how to build an "archive" page using onlyTrashed(), and how to implement restore() and forceDelete() functionality.
How to use soft deletes in Laravel
Let's return to the video tutorial to see these query and restore methods in action.
Watch from 03:08 to 05:34 to see how onlyTrashed() is used to create an archive page. Then, watch from 07:57 to 10:14 to see how to implement the restore() functionality.
4. Important Considerations
While powerful, soft deletes introduce some new behaviors and edge cases you need to be aware of.
Route Model Binding
A common pitfall occurs with route model binding. By default, if you type-hint a model for a route, Laravel will automatically try to find it using findOrFail(). This query will fail for soft-deleted models, resulting in a 404 error, even if you want to perform an action like restore or forceDelete.
To solve this, you can chain the withTrashed() method to your route definition.
How to use soft deletes in Laravel
This is a frequent source of confusion. The video demonstrates the problem and the simple solution perfectly.
Watch from 05:34 to 07:57. Pay close attention to the 404 error that occurs and how adding ->withTrashed() to the route definition in routes/web.php resolves it.
Example from the video:
// In routes/web.php or routes/api.php
Route::post('cars/{car}/restore', [CarController::class, 'restore'])->withTrashed();
Route::delete('cars/{car}', [CarController::class, 'destroy'])->withTrashed();
Unique Constraints and Indexes
Another major consideration is database unique constraints. If you have a UNIQUE index on a column (e.g., email on a users table), and you soft-delete a user, you cannot create a new user with the same email address. The database will throw a unique constraint violation because the old record still exists.
There are a few ways to handle this:
- Validation Logic: In Laravel, you can adjust your validation rules to ignore trashed records. The
Rule::unique()helper has anignoreTrashed()method for exactly this purpose.
Eloquent Soft Deletes: Things You May Not Know
The Laravel Daily channel has a great video on advanced soft delete topics. This clip explains how to handle unique validation.
Watch from 05:50 to 07:37. This segment demonstrates the problem with unique validation and shows how to solve it using Laravel's built-in ignoreTrashed() validation rule.
-
Filtered Indexes (MSSQL): Since your goal includes mastering MSSQL, this is a highly relevant, database-level solution. MSSQL (and PostgreSQL) supports filtered indexes (or partial indexes). You can create a unique index that only applies to rows where
deleted_at IS NULL. This elegantly solves the problem at the database schema level, which is often the most robust solution.-- Example of a filtered unique index in MSSQL CREATE UNIQUE INDEX UQ_Users_Email ON dbo.Users(Email) WHERE deleted_at IS NULL;This approach is mentioned in the "Challenges of Soft Deletion" section of the Sudhakar blog post you read earlier. It's a powerful feature of modern database systems that works perfectly with the soft delete pattern.
Cascading Deletes
Standard ON DELETE CASCADE foreign key constraints do not work with soft deletes, because a soft delete is an UPDATE operation, not a DELETE. If you soft-delete a Post, its related Comments will remain, potentially becoming orphaned.
You must handle this in your application code. Common approaches include:
- Model Events/Observers: Listen for the
deleting,restoring, andforceDeletingevents and manually cascade the operation to related models. - Packages: Use a community package, like
michaeldyrynda/laravel-cascade-soft-deletes, to automate this.
Eloquent Soft Deletes: Things You May Not Know
Let's watch one more clip from the Laravel Daily video, which demonstrates what happens by default and how a package can easily solve cascading soft deletes.
Watch from 07:37 to 10:22. This shows the problem of orphaned child records and demonstrates how to use a package to cascade soft deletes to relationships.
Conclusion
You have now learned how to implement one of the most common and useful design patterns in modern web applications. Soft deletes provide a safety net, enhance data integrity, and are essential for any system requiring audit trails or data recovery features.
Let's summarize the key takeaways:
- Concept: Soft deletes mark records as deleted using a
deleted_attimestamp instead of permanently removing them. - Implementation: It requires adding a
$table->softDeletes()column in a migration and using theSoftDeletestrait in your Eloquent model. - Core Methods:
delete()to soft delete.restore()to bring a record back.forceDelete()to permanently remove it.
- Querying:
- Queries exclude trashed items by default.
withTrashed()includes them.onlyTrashed()fetches only them.
- Key Gotchas: Remember to use
->withTrashed()on routes for route model binding and to handle unique constraints and cascading deletes carefully.
You've now completed the "Advanced Eloquent ORM" module. You have a deep understanding of relationships, performance optimization, and data lifecycle management within Eloquent.
Up Next:
In our very next lesson, we will begin the "MSSQL Integration & Query Building" module. We will start by configuring your Laravel application to connect to a Microsoft SQL Server database, putting you on the path to mastering the specific database system you're targeting.