TIL #4: ActiveRecord Dependent Hooks, Callbacks, Execution Order
In today's TIL we say hi to ActiveRecord's dependent hooks, explore how they relate to callbacks, and what impact could it have on the development of an app.
ActiveRecord dependent hooks
If you use Rails's ActiveRecord to access the underlying database, you've probably found yourself often using dependent hooks. Depending on the specified option value and the type of association we are dealing with, we often end up using the dependent: :destroy or dependent: :nullify options.
What it does under the hood, is basically generate just another one before_destroy callback for your source model.
Yeah, but... why is that interesting? It's no rocket science, but I've actually recently found a bug connected with the order of these associations. Consider the following example:
class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end class User < ApplicationRecord has_many :invoice_templates, dependent: :destroy has_many :invoices, dependent: :destroy end class Invoice < ApplicationRecord belongs_to :user belongs_to :invoice_template end class InvoiceTemplate < ApplicationRecord belongs_to :user end
Now, consider we have a User record, who has one InvoiceTemplate, and a single Invoice which is connected to that InvoiceTemplate and the User. Let's also assume that you have foreign key constraints on the association columns, and... you don't have ON DELETE CASCADE rules ;)
It's now predictable what will happen, right? An
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation error comes out at you! This is the result of a very simple fact, that the order the callbacks (in our case - destroy callbacks) will be executed is identical to the order in which they are defined. And since `dependent: :destroy` creates a perfectly normal callback, ActiveRecord tried to destroy an InvoiceTemplate, while still being referenced by an Invoice. Woops!
Solution(s)? There are plenty :)
But it really depends on your use case.
- reverse the definition order of associations, so that the invoices are destroyed first
- add on_delete: :cascade to your foreign key
- add a helper has_many association on your InvoiceTemplate, which will define a before_destroy callback to destroy all assigned invoices
TIL, or Today I Learned, is where our developers share the best tech stuff they found every day. You can find smart solutions for some issues, useful advice and anything which will make your developer life easier.
Photo by Andrew Neel on Unsplash