Why polymorphic association doesn't work for S

2019-01-13 04:37发布

问题:

I have a case of polymorphic association and STI here.

# 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_classof STI models, that is in my case is Staff.

The question is: Why doesn't it work if the borrowable_type set to STI class?

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

回答1:

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


回答2:

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


回答3:

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.



回答4:

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

Tested and it works on various versions of AR.



回答5:

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.