如果多态关联的类型列不指向STI的基本模型,为什么多态关联不适用于STI?

时间:2012-03-09 03:41:27

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

我有一个多态关联和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

但只有员工实例似乎才是真的。结果是:

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

由于

7 个答案:

答案 0 :(得分:31)

好问题。我使用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

答案 1 :(得分:14)

一个较旧的问题,但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

答案 2 :(得分:11)

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

-

问题是Rails使用STI关系的base_class名称。

其他答案中记录了其原因,但要点是核心团队似乎认为您应该能够引用而不是类< / em>用于多态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

这会在将数据创建到db之前更改{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核心方法的其他部分的扩展,但似乎适用于我的本地系统。

答案 3 :(得分:5)

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

经过测试,适用于不同版本的AR。

答案 4 :(得分:1)

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

# 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

答案 5 :(得分:0)

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

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

class Firm
end

class Customer < Firm
end

class Prospect < Firm
end

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

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

我通过机会主义类型的“公司”来节省机会。 (基类)和相应的客户或潜在客户ID作为opportunistic_id。

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

答案 6 :(得分:0)

您还可以为多态类型的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),因此需要使用正确的值指定type子句。 _id只能与foreign_key声明一起使用,该声明指定多态关联的名称。

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