Deleting related models one-to-many in Laravel
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Deleting Related Models One-to-Many in Laravel: Mastering Cascading Deletes
Is this even possible? In the world of Eloquent relationships, we often focus on retrieving and inserting data, but when it comes to deletion, things can feel more complicated. You are absolutely right—most basic tutorials gloss over the actual mechanics of removing deeply nested related models. This is a common hurdle, especially when dealing with complex, multi-level hierarchies like your Package $\rightarrow$ Level $\rightarrow$ Lesson structure.
As a senior developer, I can tell you that the most efficient and "Laravel-idiomatic" way to solve this problem isn't through manual looping or writing dozens of separate queries; it’s by leveraging the power of your underlying database structure.
Let’s break down why this is necessary and how we implement true cascading deletes.
The Pitfall of Manual Deletion
When you try to delete a Package, Laravel's Eloquent will only delete that package record itself. It will leave orphaned Level records, which in turn leave orphaned Lesson records, leading to data integrity issues or application errors if you don't manually manage every single deletion step within a loop. As you correctly suspected, iterating through nested relationships and executing deletes via foreach loops is inefficient, error-prone, and scales terribly for deep hierarchies.
The Golden Rule: Database Cascading Deletes
The most robust solution bypasses the need for complex application logic entirely by delegating the cascading responsibility to the database itself. This is a fundamental concept in relational database design.
For your specific scenario (deleting a Package should automatically delete its related Levels, which in turn delete their related Lessons), you must configure your foreign keys with the ON DELETE CASCADE constraint.
Step 1: Configuring Database Constraints
When defining your migrations, ensure that for every parent-child relationship, you specify onDelete('cascade'). This tells the database engine: "If a row in the parent table is deleted, automatically delete all corresponding rows in the child table."
Here is how this translates conceptually across your models:
levelstable: Thepackage_idcolumn must have anON DELETE CASCADErule referencing thepackagestable.lessonstable: Thelevel_idcolumn must have anON DELETE CASCADErule referencing thelevelstable.
If you look at how Laravel manages these relationships, understanding the underlying database structure is key to writing performant code. When dealing with complex relationships, setting up these constraints correctly ensures that data integrity is maintained regardless of how your application interacts with it. For more information on structuring robust database interactions in Laravel, always refer back to the principles outlined by the team at laravelcompany.com.
Step 2: Implementing the Deletion in Eloquent
Once the database is correctly configured, deleting the parent model becomes a single, clean operation:
use App\Models\Package;
// When you delete this package record...
$package = Package::find(1);
if ($package) {
$package->delete(); // This triggers the database cascade!
}
Because the foreign keys are set to cascade, deleting the Package automatically cascades the deletion down to all associated Levels, and subsequently all their related Lessons. No manual querying is required.
When Cascading Isn't Possible: The Eloquent Recursive Approach (The Fallback)
In rare scenarios—perhaps due to legacy database constraints or a highly custom requirement where you need complex logic during the deletion process, rather than relying solely on the DB engine—you might need to implement recursive deletion in PHP. While less efficient than native cascading, this method provides full control:
use App\Models\Package;
use App\Models\Level;
use App\Models\Lesson;
public function deletePackageRecursively(Package $package)
{
// 1. Delete all related Levels
$package->levels()->delete();
// 2. Since deleting levels cascades to lessons (if set up correctly),
// we can now delete the levels themselves, or handle the next layer explicitly:
$package->levels()->each(function ($level) {
// Delete all related Lessons for this level
$level->lessons()->delete();
// Then delete the level itself
$level->delete();
});
// 3. Finally, delete the package
$package->delete();
}
As you can see, even in this fallback scenario, we are still relying on Eloquent's relationship methods (hasMany()->delete()) to handle the deletion at each level, which is much cleaner than writing raw SQL queries for every step. This approach demonstrates that while database constraints are king, well-structured Eloquent relationships allow you to manage complex data migrations efficiently.
Conclusion
For managing one-to-many relationships across multiple nested levels in Laravel, always prioritize setting up ON DELETE CASCADE constraints in your database migrations. This is the cleanest, most performant, and most maintainable solution. Use Eloquent's relationship methods simply to execute the parent deletion, letting the database handle the rest of the cascading work. If you must implement custom logic, use nested relationship deletions within an each() loop to maintain context, as demonstrated above. Happy coding!