touch: true, dependent: :destroy, $!
09 February 2016 on rails. 5 minutes
A little while ago our team was faced with an issue: certain DELETE requests were taking extremely long to process, to the tune of over 1 minute! This post explores what can happen when you have dependent: :destroy in a has_many
relationship
For this example I’ve extracted any domain specific implementation to a hypothetical application which manages a library:
- A library has many books.
- A book belongs to 1 library.
- A book belongs to 1 donor (the person who donated the book)
- When we update a book, we should update the
updated_at
timestamp for the donor. - When we destroy a library, we should destroy all of the books in the library.
In this example we have a library with around 2 000 books. One day I decide to delete the library. What happens in the background?
In total, this would require 4 db calls per book, plus 1 to load the Library, and 1 to delete it - a total of 8002 db calls for our example!
This is because of the dependent: :destroy from library to book - which will load and execute all callbacks for each associated book individually.
Solution
How we ended up fixing this was by reaffirming our hatred of callbacks, and by moving our deletion logic to a service. This way you can perform your business logic without letting Rails load every associated record and do the logic one-by-one. This can be done by switching out destroy
for delete_all
on has_many associations, and moving any other callback logic into the service. In this example, it may look something like:
Models:
Service
SQL when destroying a library now:
Much better! We’re performing our business logic (updating the timestamp on donor) using manageable batches of books. This will cut down the number of db calls significantly, and since we’re performing these operations on (what should be) primary keys, it will be pretty fast.
Hopefully this can be useful to someone on their next rails project.
Cheers