Callback precedence for STI models
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