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