简体   繁体   English

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

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

I have a case of polymorphic association and STI here.我这里有一个多态关联和 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

In order for the polymorphic assocation to work, according to the API documentation on Polymorphic Assocation, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations that I have to set borrowable_type to the base_class of STI models, that is in my case is Staff .为了使多态关联工作,根据关于多态borrowable_type的 API 文档, http : borrowable_type我必须将borrowable_type设置为STI 模型的base_class ,在我的例子中是Staff

The question is: Why doesn't it work if the borrowable_type set to STI class?问题是:如果将borrowable_type设置为STI 类,为什么它不起作用?

Some test to prove it:一些测试来证明它:

# 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

But it seems to be true only with Staff instance.但它似乎只适用于Staff实例。 The results are:结果是:

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

Thanks谢谢

Good question.好问题。 I had exactly the same problem using Rails 3.1.我在使用 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.显然,在 Rails 中使用多态关联和单表继承 (STI) 有点复杂。

The current Rails documentation for Rails 3.2 gives this advice for combining polymorphic associations and STI : Rails 3.2 的当前 Rails 文档给出了结合多态关联和 STI 的建议

Using polymorphic associations in combination with single table inheritance (STI) is a little tricky.将多态关联与单表继承 (STI) 结合使用有点棘手。 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.为了使关联按预期工作,请确保将 STI 模型的基本模型存储在多态关联的类型列中。

In your case the base model would be "Staff", ie "borrowable_type" should be "Staff" for all items, not "Guard".在您的情况下,基本模型将是“Staff”,即“borrowable_type”对于所有项目都应该是“Staff”,而不是“Guard”。 It is possible to make the derived class appear as the base class by using "becomes" : guard.becomes(Staff) .可以使用 "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可以将列“borrowable_type”直接设置为基类“Staff”,或者按照 Rails 文档的建议,使用自动转换它

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

An older question, but the issue in Rails 4 still remains.一个较旧的问题,但 Rails 4 中的问题仍然存在。 Another option is to dynamically create/overwrite the _type method with a concern.另一种选择是动态创建/覆盖_type方法。 This would be useful if your app uses multiple polymorphic associations with STI and you want to keep the logic in one place.如果您的应用程序使用与 STI 的多个多态关联并且您希望将逻辑保留在一个地方,这将非常有用。

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

Just had this issue in Rails 4.2 .刚刚在Rails 4.2遇到了这个问题。 I found two ways to resolve:我找到了两种解决方法:

-- ——

The problem is that Rails uses the base_class name of the STI relationship.问题是 Rails 使用了 STI 关系的base_class名称。

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.其原因已在其他答案中记录,但要点是核心团队似乎认为您应该能够引用而不是多态 STI 关联的

I disagree with this idea, but am not part of the Rails Core team, so don't have much input into resolving it.我不同意这个想法,但我不是 Rails Core 团队的一员,所以没有太多的意见来解决它。

There are two ways to fix it:有两种方法可以修复它:

-- ——

1) Insert at model-level: 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

This will change the {x}_type record before the creation of the data into the db.这将在将数据创建到数据库之前更改{x}_type记录。 This works very well, and still retains the polymorphic nature of the association.这很有效,并且仍然保留了关联的多态性。

2) Override Core ActiveRecord methods 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)

A more systemic way to fix the issue is to edit the ActiveRecord core methods which govern it.解决该问题的更系统方法是编辑管理它的ActiveRecord核心方法。 I used references in this gem to find out which elements needed to be fixed / overridden.我使用这个 gem 中的引用来找出需要修复/覆盖的元素。

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.这是未经测试的,仍然需要对 ActiveRecord 核心方法的其他一些部分进行扩展,但似乎适用于我的本地系统。

There is a gem.有一颗宝石。 https://github.com/appfolio/store_base_sti_class https://github.com/appfolio/store_base_sti_class

Tested and it works on various versions of AR.经过测试,它适用于各种版本的 AR。

You can also build a custom scope for a has_* association for the polymorphic type:您还可以为多态类型的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

Since polymorphic joins use a composite foreign key (*_id and *_type) you need to specify the type clause with the correct value.由于多态连接使用复合外键(*_id 和 *_type),因此您需要使用正确的值指定类型子句。 The _id though should work with just the foreign_key declaration specifying the name of the polymorphic association.该_id虽然应该只是工作foreign_key声明中指定的多态关联的名字。

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.由于多态的性质,知道哪些模型是可借用的可能会令人沮丧,因为它可能是您的 Rails 应用程序中的任何模型。 This relationship will need to be declared in any model where you want the cascade deletion on borrowable to be enforced.这种关系需要在您希望强制执行对可借用的级联删除的任何模型中声明。

This is how I solved that problem using aforementioned hints:这就是我使用上述提示解决该问题的方法:

# 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

and with that, for any model in which I would find belongs_to :whatever, polymorphic: true I do:有了这个,对于任何我会发现belongs_to :whatever, polymorphic: true我做的模型:

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

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:我有一个模型,其中 Firm 作为基类,Customer 和 Prospect 作为 STI 类,如下所示:

class Firm
end

class Customer < Firm
end

class Prospect < Firm
end

I also have a polymorphic class, Opportunity, which looks like this:我还有一个多态类 Opportunity,它看起来像这样:

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.我使用“公司”(基类)的 opportunistic_type 和相应的客户或潜在客户 ID 作为 opportunistic_id 保存机会。

Now I can get customer.opportunities and prospect.opportunities exactly as I want.现在我可以完全按照我的意愿获得 customer.opportunities 和prospect.opportunities。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM