Why polymorphic association doesn't work for STI if type column of the polymorphic association doesn't point to the base model of STI?

前端 未结 7 2018
旧时难觅i
旧时难觅i 2020-12-02 12:07

I have a case of polymorphic association and STI here.

# app/models/car.rb
class Car < ActiveRecord::Base
  belongs_to :borrowable, :polymorphic => tru         


        
相关标签:
7条回答
  • 2020-12-02 12:42

    Just had this issue in Rails 4.2. I found two ways to resolve:

    --

    The problem is that Rails uses the base_class name of the STI relationship.

    The reason for this has been documented in the other answers, but the gist is that the core team seem to feel that you should be able to reference the table rather than the class for a polymorphic STI association.

    I disagree with this idea, but am not part of the Rails Core team, so don't have much input into resolving it.

    There are two ways to fix it:

    --

    1) Insert at model-level:

    class Association < ActiveRecord::Base
    
      belongs_to :associatiable, polymorphic: true
      belongs_to :associated, polymorphic: true
    
      before_validation :set_type
    
      def set_type
        self.associated_type = associated.class.name
      end
    end
    

    This will change the {x}_type record before the creation of the data into the db. This works very well, and still retains the polymorphic nature of the association.

    2) Override Core ActiveRecord methods

    #app/config/initializers/sti_base.rb
    require "active_record"
    require "active_record_extension"
    ActiveRecord::Base.store_base_sti_class = false
    
    #lib/active_record_extension.rb
    module ActiveRecordExtension #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
    
      extend ActiveSupport::Concern
    
      included do
        class_attribute :store_base_sti_class
        self.store_base_sti_class = true
      end
    end
    
    # include the extension 
    ActiveRecord::Base.send(:include, ActiveRecordExtension)
    
    ####
    
    module AddPolymorphic
      extend ActiveSupport::Concern
      
      included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
        define_method :replace_keys do |record=nil|
          super(record)
          owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
        end
      end
    end
    
    ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)
    

    A more systemic way to fix the issue is to edit the ActiveRecord core methods which govern it. I used references in this gem to find out which elements needed to be fixed / overridden.

    This is untested and still needs extensions for some of the other parts of the ActiveRecord core methods, but seems to work for my local system.

    0 讨论(0)
  • 2020-12-02 12:42

    You can also build a custom scope for a has_* association for the polymorphic type:

    class Staff < ActiveRecord::Base
      has_one :car, 
              ->(s) { where(cars: { borrowable_type: s.class }, # defaults to base_class
              foreign_key: :borrowable_id,
              :dependent => :destroy
    end
    

    Since polymorphic joins use a composite foreign key (*_id and *_type) you need to specify the type clause with the correct value. The _id though should work with just the foreign_key declaration specifying the name of the polymorphic association.

    Because of the nature of polymorphism it can be frustrating to know what models are borrowables, since it could conceivably be any model in your Rails application. This relationship will need to be declared in any model where you want the cascade deletion on borrowable to be enforced.

    0 讨论(0)
  • 2020-12-02 12:50

    Good question. I had exactly the same problem using Rails 3.1. Looks like you can not do this, because it does not work. Probably it is an intended behavior. Apparently, using polymorphic associations in combination with Single Table Inheritance (STI) in Rails is a bit complicated.

    The current Rails documentation for Rails 3.2 gives this advice for combining polymorphic associations and STI:

    Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order for the associations to work as expected, ensure that you store the base model for the STI models in the type column of the polymorphic association.

    In your case the base model would be "Staff", i.e. "borrowable_type" should be "Staff" for all items, not "Guard". It is possible to make the derived class appear as the base class by using "becomes" : guard.becomes(Staff). One could set the column "borrowable_type" directly to the base class "Staff", or as the Rails Documentation suggests, convert it automatically using

    class Car < ActiveRecord::Base
      ..
      def borrowable_type=(sType)
         super(sType.to_s.classify.constantize.base_class.to_s)
      end
    
    0 讨论(0)
  • 2020-12-02 12:53

    I agree with the general comments that this ought to be easier. That said, here is what worked for me.

    I have a model with Firm as the base class and Customer and Prospect as the STI classes, as so:

    class Firm
    end
    
    class Customer < Firm
    end
    
    class Prospect < Firm
    end
    

    I also have a polymorphic class, Opportunity, which looks like this:

    class Opportunity
      belongs_to :opportunistic, polymorphic: true
    end
    

    I want to refer to opportunities as either

    customer.opportunities
    

    or

    prospect.opportunities
    

    To do that I changed the models as follows.

    class Firm
      has_many opportunities, as: :opportunistic
    end
    
    class Opportunity
      belongs_to :customer, class_name: 'Firm', foreign_key: :opportunistic_id
      belongs_to :prospect, class_name: 'Firm', foreign_key: :opportunistic_id
    end
    

    I save opportunities with an opportunistic_type of 'Firm' (the base class) and the respective customer or prospect id as the opportunistic_id.

    Now I can get customer.opportunities and prospect.opportunities exactly as I want.

    0 讨论(0)
  • 2020-12-02 12:57

    There is a gem. https://github.com/appfolio/store_base_sti_class

    Tested and it works on various versions of AR.

    0 讨论(0)
  • 2020-12-02 13:01

    An older question, but the issue in Rails 4 still remains. Another option is to dynamically create/overwrite the _type method with a concern. This would be useful if your app uses multiple polymorphic associations with STI and you want to keep the logic in one place.

    This concern will grab all polymorphic associations and ensure that the record is always saved using the base class.

    # models/concerns/single_table_polymorphic.rb
    module SingleTablePolymorphic
      extend ActiveSupport::Concern
    
      included do
        self.reflect_on_all_associations.select{|a| a.options[:polymorphic]}.map(&:name).each do |name|
          define_method "#{name.to_s}_type=" do |class_name|
            super(class_name.constantize.base_class.name)
          end
        end
      end
    end
    

    Then just include it in your model:

    class Car < ActiveRecord::Base
      belongs_to :borrowable, :polymorphic => true
      include SingleTablePolymorphic
    end
    
    0 讨论(0)
提交回复
热议问题