Почему полиморфная ассоциация не работает для ИППП, если столбец типа полиморфной ассоциации не указывает на базовую модель ИППП?
У меня есть случай полиморфной ассоциации и 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
к base_class
моделей STI, то есть в моем случае 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>.
Спасибо
Ответы
Ответ 1
Хороший вопрос. У меня была такая же проблема с Rails 3.1. Похоже, вы не можете этого сделать, потому что это не работает. Вероятно, это намеченное поведение. По-видимому, использование полиморфных ассоциаций в сочетании с однонаправленным наследованием (STI) в Rails является немного сложным.
Текущая документация Rails для Rails 3.2 дает этот совет для объединения полиморфных ассоциаций и STI:
Использование полиморфных ассоциаций в сочетании с одной таблицей Наследование (STI) немного сложно. Для того чтобы ассоциации работайте, как ожидалось, убедитесь, что вы храните базовую модель для ИППП модели в столбце типа полиморфной ассоциации.
В вашем случае базовой моделью будет "Персонал", т.е. "заемный_тип" должен быть "Персоналом" для всех предметов, а не "Охрана". Можно создать производный класс как базовый класс, используя "становится": guard.becomes(Staff)
. Можно установить столбец "заемный_тип" непосредственно в базовый класс "Персонал" или, как предлагает документация Rails, автоматически преобразовать его с помощью
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
Ответ 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 использует имя base_class
отношения STI.
Причина этого была задокументирована в других ответах, но суть в том, что основная команда, похоже, считает, что вы должны иметь возможность ссылаться на таблицу, а не на класс для полиморфной ассоциации STI.
Я не согласен с этой идеей, но не являюсь частью команды Rails Core, поэтому у меня нет большого вклада в ее устранение.
Есть два способа исправить это:
-
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
перед созданием данных в db. Это работает очень хорошо и по-прежнему сохраняет полиморфный характер ассоциации.
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
, которые определяют его. Я использовал ссылки в этот камень, чтобы выяснить, какие элементы необходимо исправлять/отменять.
Это непроверено и все еще нуждается в расширениях для некоторых других частей основных методов ActiveRecord, но, похоже, работает для моей локальной системы.
Ответ 4
Есть драгоценный камень. https://github.com/appfolio/store_base_sti_class
Протестировано и работает на разных версиях AR.
Ответ 5
Я согласен с общими замечаниями, что это должно быть проще. Тем не менее, вот что сработало для меня.
У меня есть модель с фирмой как базовый класс, а 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
Я сохраняю возможности с оппортунистическим типом "Фирмы" (базового класса) и соответствующим идентификатором клиента или перспективы как оппортунистической_ид.
Теперь я могу получить клиентские возможности и перспективы. Точно так же, как я хочу.
Ответ 6
Вы также можете создать собственную область видимости для ассоциации 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), необходимо указать предложение типа с правильным значением. Хотя foreign_key
должен работать только с объявлением foreign_key
указывающим имя полиморфной ассоциации.
Из-за природы полиморфизма может быть неприятно знать, какие модели являются заемными, поскольку это может быть любая модель в вашем приложении на Rails. Эта связь должна быть объявлена в любой модели, где вы хотите, чтобы принудительное удаление каскада на заемных.