J'ai un cas d'association polymorphe et IST ici.
# 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
Pour que l'association polymorphe fonctionne, conformément à la documentation de l'API sur l'association polymorphe, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations que j'ai définir borrowable_type
sur le base_class
des modèles STI, c’est-à-dire dans mon cas, c'est Staff
.
La question est: Pourquoi cela ne fonctionne-t-il pas si le borrowable_type
est défini sur la classe STI?
Quelques tests pour le prouver:
# 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
Mais cela semble être vrai uniquement avec Staff instance. Les résultats sont:
# 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>.
Merci
Bonne question. J'ai eu exactement le même problème avec Rails 3.1. On dirait que vous ne pouvez pas faire cela, car cela ne fonctionne pas. C'est probablement un comportement voulu. Apparemment, l’utilisation d’associations polymorphes en combinaison avec Single Table Inheritance (STI) dans Rails est un peu compliquée.
La documentation actuelle de Rails pour Rails 3.2 donne ce conseil pour combiner les associations polymorphes et STI :
L'utilisation d'associations polymorphes en combinaison avec l'héritage d'une seule table Est un peu délicate. Pour que les associations fonctionnent correctement, veillez à enregistrer le modèle de base des modèles STI Dans la colonne de type de l'association polymorphe.
Dans votre cas, le modèle de base serait "Staff", c'est-à-dire que "borrowable_type" devrait être "Staff" pour tous les éléments, et non "Guard". Il est possible de faire en sorte que la classe dérivée apparaisse comme classe de base en utilisant "devient": guard.becomes(Staff)
. Vous pouvez définir la colonne "borrowable_type" directement dans la classe de base "Staff" ou, comme le suggère la documentation Rails, la convertir automatiquement à l'aide de
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
Une question plus ancienne, mais le problème dans Rails 4 reste toujours. Une autre option consiste à créer/remplacer dynamiquement la méthode _type
par un problème. Cela serait utile si votre application utilise plusieurs associations polymorphes avec STI et que vous souhaitez conserver la logique au même endroit.
Cette préoccupation saisira toutes les associations polymorphes et garantira que l'enregistrement est toujours enregistré à l'aide de la classe de 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
Ensuite, incluez-le dans votre modèle:
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
include SingleTablePolymorphic
end
Je viens d'avoir ce problème dans Rails 4.2
. J'ai trouvé deux façons de résoudre:
-
Le problème est que Rails utilise le nom base_class
de la relation STI.
La raison de cela a été documentée dans les autres réponses, mais Gist est que l'équipe principale semble penser que vous devriez pouvoir référencer le table plutôt que le class pour un association polymorphe d'IST.
Je ne suis pas d'accord avec cette idée, mais je ne fais pas partie de l'équipe principale de Rails. Par conséquent, ne participez pas beaucoup à sa résolution.
Il y a deux façons de résoudre ce problème:
-
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
Cela modifiera l'enregistrement {x}_type
avant la création des données dans la base de données. Cela fonctionne très bien et conserve toujours la nature polymorphe de l'association.
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)
Un moyen plus systématique de résoudre le problème consiste à modifier les méthodes de base ActiveRecord
qui le régissent. J'ai utilisé des références dans this gem pour savoir quels éléments devaient être corrigés/remplacés.
Cela n'a pas été testé et nécessite toujours des extensions pour certaines des autres parties des méthodes principales d'ActiveRecord, mais semble fonctionner pour mon système local.
Il y a une gemme. https://github.com/appfolio/store_base_sti_class
Testé et cela fonctionne sur différentes versions de AR.
Vous pouvez également créer une étendue personnalisée pour une association has_*
pour le type polymorphe:
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
Etant donné que les jointures polymorphes utilisent une clé étrangère composite (* _id et * _type), vous devez spécifier la clause type avec la valeur correcte. Le _id cependant devrait fonctionner uniquement avec la déclaration foreign_key
spécifiant le nom de l'association polymorphe.
En raison de la nature du polymorphisme, il peut être frustrant de savoir ce que les modèles sont empruntables, car il pourrait éventuellement s'agir de n'importe quel modèle dans votre application Rails. Cette relation doit être déclarée dans tout modèle dans lequel vous souhaitez que la suppression en cascade sur empruntable soit appliquée.
Je suis d'accord avec les commentaires généraux selon lesquels cela devrait être plus facile. Cela dit, voici ce qui a fonctionné pour moi.
J'ai un modèle avec Firm en tant que classe de base et Client et Prospect en tant que classes STI, ainsi:
class Firm
end
class Customer < Firm
end
class Prospect < Firm
end
J'ai aussi une classe polymorphe, Opportunity, qui ressemble à ceci:
class Opportunity
belongs_to :opportunistic, polymorphic: true
end
Je veux parler d'opportunités soit
customer.opportunities
ou
prospect.opportunities
Pour ce faire, j'ai changé les modèles comme suit.
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
J'économise des opportunités avec un type opportuniste 'Firm' (la classe de base) et l'identifiant client ou prospect correspondant en tant qu'opportunistic_id.
Maintenant, je peux obtenir les opportunités client et prospectes exactement comme je le souhaite.