- Understand dependencies between models based on their relationships
- Delete child records when the associated parent record is removed
In this lesson, we'll be adding delete functionality to our API so that users can remove a dog house from the database. We'll keep using the same starter code from the previous lesson. You can set up the models with:
$ bundle install
$ rails db:migrate db:seed
In our AirBudNB app, there is a one-to-many relationship between a dog house and its reviews:
Recall that at the level of the database, this means that for every entry
in the reviews
table, there is a dog_house_id
foreign key column that points
to the row in the dog_houses
table:
Imagine we're creating a feature to give users the ability to delete a dog house from the site. When that dog house is deleted, what should happen to the reviews? Both from our users' perspective and from the database's perspective, it doesn't make much sense to keep a review around if there's no dog house for it to be associated with.
In fact, if you try removing a record from the database now, you'll see an error!
Try this out in a Rails console session with rails c
:
DogHouse.first.destroy
# => ActiveRecord::InvalidForeignKey (SQLite3::ConstraintException: FOREIGN KEY constraint failed)
The issue is that this dog house has reviews associated with it:
DogHouse.first.reviews
# => #ActiveRecord::Associations::CollectionProxy [#Review id: 1, ...
Those reviews must have a valid dog_house_id
for their foreign key because of
a database constraint that was established when we created the reviews
table:
class CreateReviews < ActiveRecord::Migration[6.1]
def change
create_table :reviews do |t|
t.string :username
t.string :comment
t.integer :rating
# foreign_key: true establishes a relationship between a review and a dog house
t.belongs_to :dog_house, null: false, foreign_key: true
t.timestamps
end
end
end
So before removing the dog house, we must first remove the reviews. We can do this manually from the Rails console:
DogHouse.first.reviews.destroy_all
Notice in the SQL generated by Active Record, this finds all the associated reviews for the dog house and deletes them from the database:
DogHouse Load (0.3ms) SELECT "dog_houses".* FROM "dog_houses" ORDER BY "dog_houses"."id" ASC LIMIT ? [["LIMIT", 1]]
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."dog_house_id" = ? [["dog_house_id", 1]]
Review Destroy (0.4ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 1]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 2]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 3]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 4]]
After deleting the reviews, we can then safely delete the dog house:
DogHouse.first.destroy
However, there is a better way!
As part of the class definition for our DogHouse
model, we included the
has_many
association reference:
# app/models/dog_house.rb
class DogHouse < ApplicationRecord
has_many :reviews
end
This is what lets us easily find all the reviews associated with a dog house
instance by simply calling .reviews
on any instance of the DogHouse
class.
The has_many
association reference also lets you provide
additional options to customize its behavior. In our case
(and in many cases involving a one-to-many relationship), we can use the
dependent: :destroy
option. This will tell Active Record
to delete all the associated records when the parent record is deleted.
Exit the Rails console session, then add this code to the DogHouse
class:
# app/models/dog_house.rb
class DogHouse < ApplicationRecord
has_many :reviews, dependent: :destroy
end
So as soon as we call .destroy
on an instance of a DogHouse
, all the reviews
associated with that instance will be destroyed! Restart the Rails console and
try it out:
DogHouse.second.destroy
No more error! We were able to delete the dog house from the database along with its associated reviews with just this one line of code. And the SQL generated by Active Record matches our two-step approach from earlier of deleting the reviews first, then deleting the dog house:
DogHouse Load (0.1ms) SELECT "dog_houses".* FROM "dog_houses" ORDER BY "dog_houses"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
TRANSACTION (0.1ms) begin transaction
Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."dog_house_id" = ? [["dog_house_id", 2]]
Review Destroy (0.4ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 5]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 6]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 7]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 8]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 9]]
Review Destroy (0.1ms) DELETE FROM "reviews" WHERE "reviews"."id" = ? [["id", 10]]
DogHouse Destroy (0.1ms) DELETE FROM "dog_houses" WHERE "dog_houses"."id" = ? [["id", 2]]
It's always a good idea to clean up any unused data in the database when
deleting records, and to make sure there aren't any records that lose a
necessary association when their parent record is deleted. With Active Record,
we can use the dependent: :destroy
option to automatically remove associated
records when the parent record is deleted.
Before you move on, make sure you can answer the following question:
- In what situations would you need to use the
dependent: :destroy
option?