TIL #4: ActiveRecord Dependent Hooks, Callbacks, Execution Order

Photo of Rafał Warzocha

Rafał Warzocha

Updated Jul 16, 2021 • 3 min read
andrew-neel-308138

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

Photo of Rafał Warzocha

More posts by this author

Rafał Warzocha

Rafal is a young person who loves solving real-life problems with code. His coding journey began...
Efficient software development  Build faster, deliver more  Start now!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business