web-dev-qa-db-fra.com

Pourquoi l'association polymorphe ne fonctionne pas pour STI si la colonne de type de l'association polymorphe ne pointe pas vers le modèle de base de STI?

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

38
Trung Lê

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
30
0x4a6f4672

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
14
alkalinecoffee

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:

-

1) Insérer au niveau du modèle:

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.

2) Ignorer les méthodes de base 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.

10
Richard Peck

Il y a une gemme. https://github.com/appfolio/store_base_sti_class

Testé et cela fonctionne sur différentes versions de AR.

5
across

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.

0
Ben Simpson

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.

0
Wes