Laravel Parent Children and Grandchildren Delete: Three Options

If you have a hasMany relationship in Laravel and want to delete children records when the parent is deleted, there are a few ways to set that up.

Option 1. Foreign Key with Cascade Delete in Migrations

If you have a project with many tasks, here's the foreign key code in the tasks migration:

$table->foreignId('project_id')->constrained()->cascadeOnDelete();

This one is the easiest and most reliable to implement, as it will get executed on the database level and not by your Laravel code.

That said, important notice: it will NOT execute any Eloquent Observer methods or Events if you have them on the deleted record.


Option 2. Deleting Manually with DB Transaction

If you don't have the cascade delete from above, you need to implement it manually: first, delete all the children records and then the parent one.

ProjectController.php:

use Illuminate\Support\Facades\DB;
 
public function destroy(Project $project) {
DB::transaction(function () {
$project->tasks()->delete();
$project->delete();
});
}

Database Transaction is important because it would rollback any deleted task if something goes wrong with the following deleting of other tasks or parent project object. So, it's "delete all or nothing".


Option 3. Deleting Individual Records for Events

Both approaches above would NOT fire any Events/Observers for deleting the tasks because it's a mass delete, not one-by-one.

Imagine if you have this Observer:

app/Observers/TaskObserver.php:

class UserObserver
{
public function deleted(User $user): void
{
// ... something like cleaning up unnecessary files
}
 
}

So, if you want that deleted() event to be triggered for every task individually, you need to delete them one by one, like this:

ProjectController.php:

use Illuminate\Support\Facades\DB;
 
public function destroy(Project $project) {
DB::transaction(function () {
$project->tasks->each->delete();
$project->delete();
});
}

This is a shorter "higher order" Collection syntax with ->each instead of performing a foreach loop. Read more here.

This approach would execute slower because it will run individual delete queries for every record, but it's necessary if you want to have some events for each of them.


What About Grandchildren: Second Level Relationship?

Imagine the same scenario if you have three levels, for example:

  • users
  • users have many projects
  • projects have many tasks

How to auto-delete all the projects and tasks when deleting the user?

The same principles apply. Option 1 is to still use ->cascadeOnDelete() on all levels.

For the second option, you just need to add a loop, like this:

UserController.php:

use Illuminate\Support\Facades\DB;
 
public function destroy(User $user) {
DB::transaction(function () {
foreach ($user->projects as $project) {
$project->tasks()->delete();
}
$user->projects()->delete();
$user->delete();
}
}

Or, following the third option, if you need Observers, you may fire individual queries, instead:

UserController.php:

use Illuminate\Support\Facades\DB;
 
public function destroy(User $user) {
DB::transaction(function () {
foreach ($user->projects as $project) {
$project->tasks->each->delete();
}
$user->projects->each->delete();
$user->delete();
}
}
avatar

Also we can use Observer to get the same result for the first example:

class ProjectObserver
{
	//public function deleting(Project $project) : void
    public function deleted(Project $project): void
    {
        $project->tasks->each->delete();
    }
 
}

but is that save as Database Transaction? I mean for "delete all or nothing".

avatar

No, that wouldn't be a database transaction, it would be a separate SQL query for each task.

avatar

Hi Povilas, I cant see any option to see all these free tutorials category from menu. I could only check this post through by your tweet. I would also like the option to bookmark or add favorite option to your couses/tutorials since there have been so many in past monts. Thanks

avatar

All new tutorials are on the homepage, just scroll down the homepage, they are in descending order.

There's also RSS feed available to be integrated into Feedly or elsewhere.

For favorites - you can use Browser Favorites function, we decided not to invest time to reinvent the wheel for this, and better focus on the content.

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1057 lessons, total 42 h 44 min)
  • 79 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials