ruby-on-rails - single - rails references
¿Por qué la asociación polimórfica no funciona para STI si la columna tipo de la asociación polimórfica no apunta al modelo base de STI? (5)
Aquí tengo un caso de asociación polimórfica e ITS.
# 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
Para que funcione la asociación polimórfica, de acuerdo con la documentación de la API sobre Asociación polimórfica, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations que tengo que establecer borrowable_type
para la base_class
de los modelos de STI, en mi caso es Staff
.
La pregunta es: ¿por qué no funciona si el tipo de borrowable_type
establece en la clase de STI?
Algunas pruebas para probarlo:
# 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
Pero parece ser cierto solo con la instancia Staff . Los resultados son:
# 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>.
Gracias
Acabo de tener este problema en Rails 4.2
. Encontré dos formas de resolver:
-
El problema es que Rails usa el nombre de base_class
de la relación de STI.
La razón de esto se ha documentado en las otras respuestas, pero la esencia es que el equipo central parece sentir que debería ser capaz de hacer referencia a la tabla en lugar de a la clase de una asociación polimérica de ITS.
No estoy de acuerdo con esta idea, pero no soy parte del equipo de Rails Core, por lo que no tengo mucha información para resolverlo.
Hay dos formas de solucionarlo:
-
1) Insertar a nivel de modelo:
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
Esto cambiará el registro {x}_type
antes de la creación de los datos en el db. Esto funciona muy bien, y aún conserva la naturaleza polimórfica de la asociación.
2) Reemplazar los métodos Core 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://.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://.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)
Una forma más sistémica de solucionar el problema es editar los métodos centrales de ActiveRecord
que lo rigen. Usé referencias en esta gema para descubrir qué elementos necesitaban ser reparados / reemplazados.
Esto no se ha probado y aún necesita extensiones para algunas de las otras partes de los métodos básicos de ActiveRecord, pero parece funcionar para mi sistema local.
Buena pregunta. Tenía exactamente el mismo problema con Rails 3.1. Parece que no puedes hacer esto, porque no funciona. Probablemente es un comportamiento intencionado. Aparentemente, usar asociaciones polimórficas en combinación con herencia de tabla única (ITS) en Rails es un poco complicado.
La documentación actual de Rails para Rails 3.2 brinda este consejo para combinar http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations :
Usar asociaciones polimórficas en combinación con la herencia de tablas únicas (STI) es un poco complicado. Para que las asociaciones funcionen como se esperaba, asegúrese de almacenar el modelo base para los modelos de STI en la columna de tipo de la asociación polimórfica.
En su caso, el modelo base sería "Staff", es decir, "borrowable_type" debería ser "Staff" para todos los artículos, no "Guard". Es posible hacer que la clase derivada aparezca como la clase base usando "se convierte": guard.becomes(Staff)
. Se podría establecer la columna "borrowable_type" directamente en la clase base "Staff", o como sugiere la documentación de Rails, convertirla automáticamente usando
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
Estoy de acuerdo con los comentarios generales de que esto debería ser más fácil. Dicho eso, esto es lo que funcionó para mí.
Tengo un modelo con Firma como clase base y Cliente y Prospect como las clases de STI, así:
class Firm
end
class Customer < Firm
end
class Prospect < Firm
end
También tengo una clase polimórfica, Opportunity, que se ve así:
class Opportunity
belongs_to :opportunistic, polymorphic: true
end
Quiero referirme a las oportunidades como
customer.opportunities
o
prospect.opportunities
Para hacer eso, cambié los modelos de la siguiente manera.
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
Ahorro oportunidades con un oportunistic_type de ''Firm'' (la clase base) y el cliente respectivo o el ID de cliente potencial como el opportunistic_id.
Ahora puedo obtener oportunidades de cliente y oportunidades potenciales exactamente como yo quiero.
Hay una gema https://github.com/appfolio/store_base_sti_class
Probado y funciona en varias versiones de AR.
Una pregunta anterior, pero el problema en Rails 4 aún permanece. Otra opción es crear / sobreescribir dinámicamente el método _type
con una preocupación. Esto sería útil si su aplicación utiliza múltiples asociaciones polimórficas con STI y desea mantener la lógica en un solo lugar.
Esta preocupación atrapará todas las asociaciones polimórficas y asegurará que el registro siempre se guarde utilizando la clase base.
# 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
Luego solo inclúyelo en tu modelo:
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
include SingleTablePolymorphic
end