【问题标题】:Why polymorphic association doesn't work for STI if type column of the polymorphic association doesn't point to the base model of STI?如果多态关联的类型列不指向 STI 的基本模型,为什么多态关联对 STI 不起作用?
【发布时间】:2012-03-26 13:58:39
【问题描述】:

我这里有一个多态关联和 STI 的案例。

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

# app/models/staff.rb
class Staff < ActiveRecord::Base
  has_one :car, :as => :borrowable, :dependent => :destroy
end

# app/models/guard.rb
class Guard < Staff
end

为了使多态关联工作,根据多态关联的 API 文档,http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations 我必须将 borrowable_type 设置为 STI 模型的 base_class,在我的情况下是 Staff .

问题是:如果 borrowable_type 设置为 STI 类,为什么它不起作用?

一些测试来证明这一点:

# now the test speaks only truth

# test/fixtures/cars.yml
one:
  name: Enzo
  borrowable: staff (Staff)

two:
  name: Mustang
  borrowable: guard (Guard)

# test/fixtures/staffs.yml
staff:
  name: Jullia Gillard

guard:
  name: Joni Bravo
  type: Guard 

# test/units/car_test.rb

require 'test_helper'

class CarTest < ActiveSupport::TestCase
  setup do
    @staff = staffs(:staff)
    @guard = staffs(:guard) 
  end

  test "should be destroyed if an associated staff is destroyed" do
    assert_difference('Car.count', -1) do
      @staff.destroy
    end
  end

  test "should be destroyed if an associated guard is destroyed" do
    assert_difference('Car.count', -1) do
      @guard.destroy
    end
  end

end

但似乎只有 Staff 实例才是正确的。结果是:

# Running tests:

F.

Finished tests in 0.146657s, 13.6373 tests/s, 13.6373 assertions/s.

  1) Failure:
test_should_be_destroyed_if_an_associated_guard_is_destroyed(CarTest) [/private/tmp/guineapig/test/unit/car_test.rb:16]:
"Car.count" didn't change by -1.
<1> expected but was
<2>.

谢谢

【问题讨论】:

    标签: ruby-on-rails activerecord polymorphic-associations


    【解决方案1】:

    好问题。我在使用 Rails 3.1 时遇到了完全相同的问题。看起来你不能这样做,因为它不起作用。可能这是一种预期的行为。显然,在 Rails 中结合使用多态关联和单表继承 (STI) 有点复杂。

    Rails 3.2 的当前 Rails 文档给出了合并 polymorphic associations and STI 的建议:

    结合单表使用多态关联 继承(STI)有点棘手。为了让协会 按预期工作,确保存储 STI 的基本模型 多态关联的类型列中的模型。

    在您的情况下,基本模型将是“Staff”,即所有项目的“borrowable_type”应该是“Staff”,而不是“Guard”。可以使用“成为”使派生类显示为基类:guard.becomes(Staff)。可以将列“borrowable_type”直接设置为基类“Staff”,或者按照 Rails 文档的建议,使用自动转换它

    class Car < ActiveRecord::Base
      ..
      def borrowable_type=(sType)
         super(sType.to_s.classify.constantize.base_class.to_s)
      end
    

    【讨论】:

    • 这意味着您不能将 Car 与 Guard 关联并使用 @guard.car 检索它,因为 Car 表的“borrowable_type”列将始终设置为“Staff”,而永远不会设置为“Guard” '。这意味着 STI 模型上的多态关联完全没有用。
    • 有可能,这个答案对我有用。嗯......奇怪的是它不能开箱即用。我不明白为什么。
    • @dekeguard 您关于@guard.car 的声明是错误的。使用提供的解决方案,Rails 透明地使用 STI 基类名 Staff 作为目标类型,并且由于 id 在 STI 表中始终是唯一的,因此将类型设置为GuardStaff 在 poly 表中将获取正确的记录。从@car.guard 出发也将返回相应的Guard。因此,STI 对多态性很有用,因为 STI 模型不仅可以工作(使用提供的解决方案)其他模型仍然可以根据需要与多态模型相关。跨度>
    【解决方案2】:

    一个较老的问题,但 Rails 4 中的问题仍然存在。另一种选择是动态地创建/覆盖 _type 方法。如果您的应用使用多个与 STI 的多态关联并且您希望将逻辑保留在一个位置,这将非常有用。

    这个关注点将抓取所有多态关联并确保始终使用基类保存记录。

    # 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
    

    然后将其包含在您的模型中:

    class Car < ActiveRecord::Base
      belongs_to :borrowable, :polymorphic => true
      include SingleTablePolymorphic
    end
    

    【讨论】:

    【解决方案3】:

    刚刚在Rails 4.2 中遇到了这个问题。我找到了两种解决方法:

    --

    问题在于 Rails 使用了 STI 关系的 base_class 名称。

    原因已在其他答案中记录,但要点是核心团队似乎认为您应该能够引用 table 而不是 class 用于多态性 STI 关联。

    我不同意这个想法,但我不是 Rails 核心团队的一员,所以没有太多意见来解决它。

    有两种修复方法:

    --

    1) 在模型级别插入:

    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
    

    这将在创建数据到数据库之前更改{x}_type 记录。这工作得很好,并且仍然保留了关联的多态性。

    2) 覆盖核心ActiveRecord 方法

    #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)
    

    解决问题的更系统的方法是编辑管理它的ActiveRecord 核心方法。我使用this gem 中的引用来找出需要修复/覆盖的元素。

    这是未经测试的,仍然需要扩展 ActiveRecord 核心方法的其他部分,但似乎适用于我的本地系统。

    【讨论】:

    【解决方案4】:

    有一颗宝石。 https://github.com/appfolio/store_base_sti_class

    经过测试,适用于各种版本的 AR。

    【讨论】:

      【解决方案5】:

      您还可以为多态类型的has_* 关联构建自定义范围:

      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
      

      由于多态连接使用复合外键(*_id 和 *_type),您需要使用正确的值指定类型子句。 _id 应该只与指定多态关联名称的 foreign_key 声明一起使用。

      由于多态性的本质,知道什么模型是可借用的可能会令人沮丧,因为可以想象它可以是 Rails 应用程序中的任何模型。这种关系需要在您希望对可借用的级联删除强制执行的任何模型中声明。

      【讨论】:

      • 谢谢,这可行
      【解决方案6】:

      这就是我使用上述提示解决该问题的方法:

      # app/models/concerns/belongs_to_single_table_polymorphic.rb
      
      module BelongsToSingleTablePolymorphic
        extend ActiveSupport::Concern
      
        included do
          def self.belongs_to_sti_polymorphic(model)
            class_eval "belongs_to :#{model}, polymorphic: true"
            class_eval 'before_validation :set_sti_object_type'
      
            define_method('set_sti_object_type') do
              sti_type = send(model).class.name
      
              send("#{model}_type=", sti_type)
            end
          end
        end
      end
      
      

      然后,对于我可以找到belongs_to :whatever, polymorphic: true 的任何型号,我都会这样做:

      class Reservation < ActiveRecord::Base
        include BelongsToSingleTablePolymorphic
        # .....
        belongs_to_sti_polymorphic :whatever
        # .....
      end
      

      【讨论】:

        【解决方案7】:

        我同意一般 cmets 的观点,认为这应该更容易。也就是说,这对我有用。

        我有一个模型,其中 Firm 作为基类,Customer 和 Prospect 作为 STI 类,如下所示:

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

        我还有一个多态类 Opportunity,它看起来像这样:

        class Opportunity
          belongs_to :opportunistic, polymorphic: true
        end
        

        我想将机会称为两者之一

        customer.opportunities
        

        prospect.opportunities
        

        为此,我按如下方式更改了模型。

        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
        

        我使用“Firm”(基类)的 opportunistic_type 并将相应的客户或潜在客户 ID 作为 opportunistic_id 来保存机会。

        现在我可以完全按照自己的意愿获取 customer.opportunities 和prospect.opportunities。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-05-03
          • 2020-07-08
          • 2011-09-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多