I have a model, Guest
which takes nested attributes for Numberplate
.
Guest
adopts STI (single table inheritance) and inherits from Person
. To prevent the need for extra queries or joining to another table I save the person type in a field called person_type
in the Numberplate
table. That way, I know which Numberplates are owned by Guests without the need of joining with the persons table.
Due to this setup, I need to manually set the correct person_type
on a Numberplate
(there is another person type). I implemented this as a before_validation
callback on the Numberplate
model:
before_validation do
self.person_type = self.person.type
end
The actual implementation is a bit more generic but it boils down to this.
Now I would create a Guest
and Numberplate
in one call in the console this all works as expected:
Numberplate.new({person: Guest.new({company: Company.first}), plate: 'EFEF98'})
The person_type
on the Numberplate
instance is correctly set to "Guest"
.
The issue arises when I have an API endpoint that allows nested attributes to create the Guest
and Numberplate
in one request. The strange thing is that the person_type
gets stored as "Person'
and not "Guest"
as expected. I debugged a bit and the person_type
gets set correctly in the before_validation
callback but between that callback and the before_save
callback the person_type
is suddenly changed to "Person"
.
I tried to override the person_type
setter and log when it gets set. The strange thing is that the setter is not called between the 2 ActiveRecord callbacks.
I'm a bit flabbergasted as to how this behavior is introduced, and whether this is a bug in the Rails core somewhere or I'm not accounting for something. My current workaround is to also set the person_type
in an additional before_save
callback but this doesn't feel like an optimal solution.
Relevant models:
class Guest < Person
accepts_nested_attributes_for :numberplates
end
class Numberplate < ApplicationRecord
before_validation do
self.person_type = self.person.type
end
after_validation do
# person_type is correctly set to "Guest"
puts self.inspect
end
before_create do
# person_type is now suddenly set to "Person"
puts self.inspect
end
end
So I figured it out.
In the Person
class I had this:
has_many :numberplates, as: :person, inverse_of: :person
The as: :person
part should not be there and introduced this behavior. It was there due to an earlier migration from polymorphism to STI and was thus a forgotten leftover. Once I removed this everything worked as expected.