3

Let's assume I have a Book. This Book has Chapters and those Chapters in this Book have Subchapters.

So I have three models:

Book > Chapter > Subchapter

When I delete the Book ($book->delete();), I also want to delete the related Chapters of this Book and also the related Subchapters of all the Chapters from the Book.

Here (Automatically deleting related rows in Laravel (Eloquent ORM)) I found out about Eloquent Events. Whenever a Book is deleted, before that, the Chapter gets deleted because we hook in:

class Book extends Eloquent
{
    public function chapters()
    {
        return $this->has_many('Chapter');
    }

    protected static function boot() {
        parent::boot();

        static::deleting(function($book) { 
             $book->chapters()->delete();
        });
    }
}

So I thought, I only have to implement the same code in my Chapter-Model, only exchanging "Book" with "Chapter" and "Chapter" with "Subchapter":

class Chapter extends Eloquent
{
    public function subChapters()
    {
        return $this->has_many('SubChapter');
    }

    protected static function boot() {
        parent::boot();

        static::deleting(function($chapter) { 
             $chapter->subChapters()->delete();
        });
    }
}

This works fine when I delete a Chapter. All the Subchapters will also be deleted.

When I delete the Book it works fine with the Chapters. All the Chapters will also be deleted.

However it only deletes Chapters when I delete the Book. It does not delete the related Subchapters of the deleted Chapters.

Can anybody help me?

Mydisname
  • 213
  • 1
  • 4
  • 13
  • Do it on RDBMS level. – Kyslik Jul 13 '17 at 09:04
  • Try with the migration option (on cascade delete) – ka_lin Jul 13 '17 at 09:04
  • Try to loop over the chapters and delete one by one `foreach ($book->chapters()->get() as $chapter) { $chapter->delete(); }` and the same in the subChapters `foreach ( $chapter->subChapters()->get() as $subChapter) { $subChapter->delete(); }` – Maraboc Jul 13 '17 at 09:05

3 Answers3

5

That's because when you delete multiple objects at the same time it doesn't trigger the boot deleting function for each model so you should loop through the objects and delete them one by one:

class Book extends Eloquent
{
  public function chapters()
   {
    return $this->has_many(Chapter::class);
   }

  protected static function boot() {
    parent::boot();

    static::deleting(function($book) { 
        foreach($book->chapters as $chapter){
          $chapter->delete();
        }
    });
  }
}
/********************************/
 class Chapter extends Eloquent
{
  public function subChapters()
   {
    return $this->has_many(SubChapter::class);
   }

  protected static function boot() {
    parent::boot();

    static::deleting(function($chapter) { 
        foreach($chapter->subChapters as $subChapter){
          $subChapter->delete();
        }
    });
  }
}

However my recommendation is to set cascading foreign key relation between the tables so the DBMS is going to delete the related rows automatically, the sample code below shows you how to do it in migration files:

 Schema::create('chapters', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('book_id')->unsigned();
        $table->string('title');
        $table->timestamps();


        $table->foreign('book_id')
        ->references('id')
        ->on('books')
        ->onDelete('cascade')
        ->onUpdate('cascade');  
    });

Do the same for subChapters. Hope It helps...

Tohid Dadashnezhad
  • 1,808
  • 1
  • 17
  • 27
3

This is because $book->chapters() will just return an instance of Builder. When you call delete() on the builder it will simply just run the query needed to delete those models and not new them up and then delete them which will mean that the deleting event on the Chapter will never be fired.

To get around this you can do something like:

protected static function boot() {

    parent::boot();

    static::deleting(function($book) { 

        $book->chapters->each(function ($chapter) {
            $chapter->delete();
        });

    });
}

Hope this helps!

Rwd
  • 34,180
  • 6
  • 64
  • 78
  • Thanks, Ross. This makes sense. Read more about it here: https://laravel-news.com/higher-order-messaging. Nevertheless, it wont work for me: Undefined property: Illuminate\Database\Eloquent\Relations\HasMany::$each seems like it does not recognize "each->". Did I forget something? – Mydisname Jul 13 '17 at 09:23
  • @andydotlutz Ah, fair enough. In that case I would just use the normal `each()` method. I've edited my answer to reflect this. – Rwd Jul 13 '17 at 10:30
  • @Muhammad In the OP's question, the `Chapter` model also has a `deleting` hook. The method someone takes to deleting related models will depend on whether they want to simply delete the rows from the database or they need to hook into the Eloquent events. – Rwd Feb 24 '21 at 08:10
2

The correct way to do that is to use onDelete('cascade'). In the chapters table migration:

$table->foreign('book_id')
      ->references('id')
      ->on('books')
      ->onDelete('cascade');

In the subchapters migration:

$table->foreign('chapter_id')
      ->references('id')
      ->on('chapters')
      ->onDelete('cascade');

In this case, you don't need to write any code and all chapters and subchapters will be deleted automatically.

Alexey Mezenin
  • 158,981
  • 26
  • 290
  • 279