ruby-on-railsrubypolymorphic-associationsruby-on-rails-7sti

Rails STI through model using polymorphic - populates source_type from parent class


Using Rails 7

I am using a Single Table Inheritance (STI) to store some very simple associations. The source object uses has_many associations with the STI models. Following some advice in question https://stackoverflow.com/a/45681641/1014251 I am using a polymorphic relationship in the join model. This works really well with one annoyance:

When creating a join models the source type is being taken from the root STI class rather than the actual source.

The models:

class Guidance < ApplicationRecord
  has_many :guidance_details
  has_many :themes, through: :guidance_details, source: :detailable, source_type: "Theme"
end

class Detail < ApplicationRecord
  has_many :guidance_details, as: :detailable
  has_many :guidances, through: :guidance_details
end

class GuidanceDetail < ApplicationRecord
  belongs_to :detailable, polymorphic: true
  belongs_to :guidance
end

class Theme < Detail
end

The problem

If I create a new GuidanceDetail and do not specify the detailable_source the system inserts "Detail" rather than "Theme".:

guidance_detail = GuidanceDetail.create(guidance: Guidance.first, detailable: Theme.first)
guidance_detail.detailable_type => "Detail"

The detailable type should be "Theme".

To fix this currently I am having to specify the detailable_type each time I create a new GuidanceDetail.

A fix that doesn't work

I have tried specifying the has_many association of the child object directly, but get the same result:

class Theme < Detail
  has_many :guidance_details, as: :detailable
end

Alternative creation method doesn't work

theme = Theme.first
guidance = Guidance.first

guidance.themes << theme

Outputs:

GuidanceDetail Create (1.3ms)  INSERT INTO "guidance_details" ("detailable_id", "guidance_id", "position", "created_at", "updated_at", "detailable_type") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["detailable_id", 11], ["guidance_id", 1], ["position", nil], ["created_at", "2024-11-14 10:24:19.785623"], ["updated_at", "2024-11-14 10:24:19.785623"], ["detailable_type", "Detail"]]

As you see: "detailable_type" is "Detail".


Solution

  • From my understanding, in order to store the class name "Theme", it appears Detail needs to be a abstract_class.

    ActiveRecord::Associations::BelongsToPolymorphicAssociation sets the foreign_type to the polymorphic_name

    polymorphic_name is defined as:

    def polymorphic_name
      store_full_class_name ? base_class.name : base_class.name.demodulize
    end
    

    and base_class

    def set_base_class # :nodoc:
      @base_class = if self == Base
        self
      else
        unless self < Base
          raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
        end
    
        if superclass == Base || superclass.abstract_class?
          self
        else
          superclass.base_class
        end
      end
    end
    

    So you can see that base_class.name will only use the actual class name if the class is the base class or the superclass is an abstract class. At least this is how I tracked it around.