Search code examples
ruby-on-railsactiverecordsingle-table-inheritance

Model with nested attribute creation changes association type attribute between before_validation and before_save


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

Solution

  • 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.