Callback precedence for STI models

November 09, 2016 | 2 Minute Read

ActiveRecord callbacks are very useful in organizing dependency relations between models. But it is very easy to get tangled up in an incorrect callback sequence, especially for single table inheritance cases.

Let's say we have a simple app that creates, lists and deletes vehicles. To keep things simple let's assume there are three models: a Vehicle model that acts as an abstract model, a Bicycle model that inherits from Vehicle and a Wheel model that has a one-to-many relationship with any Vehicle.

class Vehicle < ActiveRecord::Base
  has_many :wheels, dependent: :destroy
end
class Bicycle < Vehicle
  before_destroy :count_wheels

  def count_wheels
    puts wheels.count
  end
end
class Wheel < ActiveRecord::Base
  belongs_to :vehicle

  def do_something
    'Done!'
  end
end

So to sum up when a vehicle is destroyed, depending wheels should also be deleted. This is a general rule for all vehicles. But specifically when bicycles are destroyed we would like to count and report the number of wheels just before destruction.

bike = Bicycle.create age: 2
4.times { bike.wheels.create diameter: 50 }
bike.destroy
#=> count reported is 0

But why? We just wanted to check the number of the wheels that will be destroyed, within a before_destroy callback.

The problem here is that when models are chained to each other in an inheritance relationship, the parent callbacks take precedence over the children's. So a solution for this is to instead separate the has_many :wheels, dependent: :destroy association and the dependent destroy callback on wheels:

class Vehicle < ActiveRecord::Base
  has_many :wheels

  after_destroy :destroy_wheels

  def destroy_wheels
    wheels.destroy
  end
end

Now we can access the wheel records before they are destroyed because the Bicycle model defines the wheel count method as a before_destroy callback.

bike.destroy
#=> count reported is 4

But personally I don't feel comfortable with this solution as it is not very DRY. So I came across another solution which I believe is not very well known: the prepend option.

Instead of changing the Vehicle model all we need to do is to add an option to the Bicycle callback:

class Bicycle < Vehicle
  before_destroy :count_wheels, prepend: true

  def count_wheels
    puts wheels.count
  end
end
bike.destroy
#=> count reported is 4

Isn't that a relief? :)

Here is the documentation I found for this