TIL #4: ActiveRecord Dependent Hooks, Callbacks, Execution Order
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