The Behaviors of Dependent Destroy on Rails ActiveModel

the-behaviors-of-dependent-destroy

what is dependent :destroy

Dependent is an option of Rails collection association declaration to cascade the delete action. The :destroy is to cause the associated object to also be destroyed when its owner is destroyed.

Code Preparation

First, the DB shemas and Rails Model should be ready for the following experiment.

rails g model books
rails g model authors
rails g model comments
rails g model authors_books

rails g migration CreateJoinTableBooksAuthors books authors

Migrations

The migration files should be looked like as listed.

# db/migrate/2019_create_join_table_books_authors.rb

class CreateJoinTableBooksAuthors < ActiveRecord::Migration[6.0]
  def change
    create_table :authors_books do |t|
      t.integer "book_id", null: false
      t.integer "author_id", null: false

      t.index [:book_id, :author_id], unique: true
      t.index [:author_id, :book_id]

      t.timestamps
    end
  end
end
# db/migrate/2019_create_books.rb

class CreateBooks < ActiveRecord::Migration[6.0]
  def change
    create_table :books do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end
# db/migrate/2019_create_authors.rb

class CreateAuthors < ActiveRecord::Migration[6.0]
  def change
    create_table :authors do |t|
      t.string :name, index: { unique: true }, null: false
      t.timestamps
    end
  end
end
# db/migrate/2019_create_comments.rb

class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.integer :book_id, null: false
      t.string :content, null: false
      t.timestamps
    end
  end
end
# db/migrate/2019_create_details.rb

class CreateDetails < ActiveRecord::Migration[6.0]
  def change
    create_table :details do |t|
      t.integer :book_id, null: false
      t.integer :paperback, null: false
      t.string :publisher, null: false

      t.timestamps
    end
  end
end

Run the db migration commands

rails db:create
rails db:migrate

rails-db-migration

Models

Then update the model files for querying.

# app/models/author.rb

class Author < ApplicationRecord
  has_many :authors_books
  has_many :books, through: :authors_books, dependent: :destroy
end
# app/models/authors_book.rb

class AuthorsBook < ApplicationRecord
  belongs_to :author
  belongs_to :book

  validates :author, :book, presence: true
end
# app/models/book.rb

class Book < ApplicationRecord
  has_many :comments, dependent: :destroy
  has_many :authors_books
  has_many :authors, through: :authors_books, dependent: :destroy
  has_one :detail, dependent: :destroy 
end
# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :book
end
# app/models/detail.rb

class Detail < ApplicationRecord
  belongs_to :book, dependent: :destroy
end

How Dependent :destroy Working

One-to-One Association

Has_one & belongs_to

book = Book.create name: Faker::Book.title
Detail.create book_id: book.id, paperback: rand(330..2_000), publisher: Faker::Company.name
book.destroy
book.detail.destroy

If you want to keep the strict one to one relations betweens books and details, you can add dependent: :destroy on both sides to remove the combined records. On contract, the book record won't be deleted when the detail record is destroyed.

one-to-one-active-model

One-to-Many Association

Has_many & belongs to

book = Book.create name: Faker::Book.title
3.times { Comment.create content: Faker::Quotes::Shakespeare.hamlet_quote, book_id: book.id }
book.comments
# book.destroy
# book.comments.first.destroy

Books

ID NAME CREATED_AT UPDATED_AT
3 Arms and the Man 2019-06-24 14:05:05 2019-06-24 14:05:05

Comments

ID BOOK_ID CONTENT CREATED_AT UPDATED_AT
4 3 A little more than kin, and... 2019-06-24 14:05:05 2019-06-24 14:05:05
5 3 This above all: to thine ow... 2019-06-24 14:05:05 2019-06-24 14:05:05
6 3 The lady doth protest too m... 2019-06-24 14:05:05 2019-06-24 14:05:05

What happens when the owner record is deleted

If the owner record is delete, all the assocated records will be deleted immediately. Such as, if the a book row is delete in the books table, all the related coments will be delete as well. But, if a comment is deleted, the related book row won't be delete.

one-to-many-active-model

# has_many :comments
book = Book.create name: Faker::Book.title
3.times { Comment.create content: Faker::Quotes::Shakespeare.hamlet_quote, book_id: book.id }
# Book.first.destroy!
irb(main):012:0> Book.first.destroy!
  Book Load (0.2ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.1ms)  begin transaction
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" INNER JOIN "authors_books" ON "authors"."id" = "authors_books"."author_id" WHERE "authors_books"."book_id" = ?  [["book_id", 1]]
  Book Destroy (1.4ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 1]]
   (0.8ms)  commit transaction
=> #<Book id: 1, name: "A Farewell to Arms", created_at: "2019-06-14 09:28:35", updated_at: "2019-06-14 09:28:35">
has_many :comments, dependent: :destroy 
Book.first.comments
ID BOOK_ID CONTENT CREATED_AT UPDATED_AT
13 6 There is nothing either goo... 2019-06-24 14:10:24 2019-06-24 14:10:24
14 6 Though this be madness, yet... 2019-06-24 14:10:24 2019-06-24 14:10:24
15 6 This above all: to thine ow... 2019-06-24 14:10:24 2019-06-24 14:10:24
Book.first.destroy!
   (0.1ms)  SELECT sqlite_version(*)
  Book Load (0.1ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
   (0.1ms)  begin transaction
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."book_id" = ?  [["book_id", 4]]
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" INNER JOIN "authors_books" ON "authors"."id" = "authors_books"."author_id" WHERE "authors_books"."book_id" = ?  [["book_id", 4]]
  Book Destroy (0.2ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 4]]
   (0.9ms)  commit transaction
=> #<Book id: 4, name: "Blood's a Rover", created_at: "2019-06-14 09:28:38", updated_at: "2019-06-14 09:28:38">

Many-to-Many Association

Has_many through a Pivot Table

Mock Data

Author.delete_all
Book.delete_all

Author.create name: Faker::Book.author
Author.create name: Faker::Book.author
4.times { Book.create name: Faker::Book.title }

Author.first.books << Book.first
Author.first.books << Book.second
Author.second.books << Book.third
Author.second.books << Book.fourth

Author

ID NAME CREATED_AT UPDATED_AT
1 Mrs. Willard Balistreri 2019-06-24 14:11:17 2019-06-24 14:11:17

Books

ID NAME CREATED_AT UPDATED_AT
7 Fear and Trembling 2019-06-24 14:11:17 2019-06-24 14:11:17
8 The Proper Study 2019-06-24 14:11:17 2019-06-24 14:11:17
9 After Many a Summer Dies th... 2019-06-24 14:11:17 2019-06-24 14:11:17
10 Tirra Lirra by the River 2019-06-24 14:11:17 2019-06-24 14:11:17

AuthorsBooks

ID BOOK_ID AUTHOR_ID CREATED_AT UPDATED_AT
1 7 1 2019-06-24 14:11:17 2019-06-24 14:11:17
2 8 1 2019-06-24 14:11:17 2019-06-24 14:11:17
3 9 2 2019-06-24 14:11:17 2019-06-24 14:11:17
4 10 2 2019-06-24 14:11:17 2019-06-24 14:11:17

Delete

Author.second.books
Author.first.authors_books
Author.first.books.first.destroy
# AuthorsBook.find(14).destroy

Removing records from the pivot table , only applied on that pivot table, no assocated records will be delete. In the case, authors and books won't be deleted when deleting authors_books rows.

If a assocated record is delete, the relation rows in that pivot table will also be deleted and all these operations are wrapped in a transaction.

many-to-many-active-model

Conclusion

In one -to-one, and one-to-many scenarios, has_one or has_many, the associated records will be deleted if the owner record got deleted. But, With has_and_belongs_to_many and has_many :through, the many to many scenario, the join records will be deleted, but the associated records won't.