web-dev-qa-db-fra.com

Rails Association polymorphe avec plusieurs associations sur le même modèle

Ma question est essentiellement la même que celle-ci: Association polymorphe avec plusieurs associations sur le même modèle

Cependant, la solution proposée/acceptée ne fonctionne pas, comme illustré par un commentateur plus tard.

J'ai un cours photo qui est utilisé partout dans mon application. Un message peut avoir une seule photo. Cependant, je veux réutiliser la relation polymorphe pour ajouter une photo secondaire.

Avant:

class Photo 
   belongs_to :attachable, :polymorphic => true
end

class Post
   has_one :photo, :as => :attachable, :dependent => :destroy
end

Voulu:

class Photo 
   belongs_to :attachable, :polymorphic => true
end

class Post
   has_one :photo,           :as => :attachable, :dependent => :destroy
   has_one :secondary_photo, :as => :attachable, :dependent => :destroy
end

Cependant, cela échoue car il ne peut pas trouver la classe "SecondaryPhoto". Sur la base de ce que je pouvais dire de cet autre fil, je voudrais faire:

   has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy

Sauf en appelant Post # secondary_photo renvoie simplement la même photo qui est jointe via l'association Photo, par ex. Poster # photo === Poster # secondaire_photo. En regardant le SQL, il fait O type type = "Photo" au lieu de, disons, "SecondaryPhoto" comme je voudrais ...

Pensées? Merci!

56
Matt Rogish

Je l'ai fait dans mon projet.

L'astuce est que les photos ont besoin d'une colonne qui sera utilisée dans une condition has_one pour faire la distinction entre les photos primaires et secondaires. Faites attention à ce qui se passe dans :conditions ici.

has_one :photo, :as => 'attachable', 
        :conditions => {:photo_type => 'primary_photo'}, :dependent => :destroy

has_one :secondary_photo, :class_name => 'Photo', :as => 'attachable',
        :conditions => {:photo_type => 'secondary_photo'}, :dependent => :destroy

La beauté de cette approche est que lorsque vous créez des photos à l'aide de @post.build_photo, le photo_type sera automatiquement prérempli avec le type correspondant, comme 'primary_photo'. ActiveRecord est suffisamment intelligent pour cela.

69
Max Chernyak

future référence pour les personnes vérifiant ce message

Ceci peut être réalisé en utilisant le code suivant ...

Rails 3:

has_one :banner_image, conditions: { attachable_type: 'ThemeBannerAttachment' }, class_name: 'Attachment', foreign_key: 'attachable_id', dependent: :destroy

Rails 4:

has_one :banner_image, -> { where attachable_type: 'ThemeBannerAttachment'}, class_name: 'Attachment', dependent: :destroy

Je ne sais pas pourquoi, mais dans Rails 3, vous devez fournir une valeur foreign_key à côté des conditions et class_name. N'utilisez pas 'as:: attachable' car cela utilisera automatiquement le nom de la classe appelante lorsque définition du type polymorphe.

Ce qui précède s'applique également à has_many.

5
JellyFishBoy

Quelque chose comme le suivant a fonctionné pour l'interrogation, mais l'attribution de l'utilisateur à l'adresse n'a pas fonctionné

Classe d'utilisateurs

has_many :addresses, as: :address_holder
has_many :delivery_addresses, -> { where :address_holder_type => "UserDelivery" },
       class_name: "Address", foreign_key: "address_holder_id"

Classe d'adresse

belongs_to :address_holder, polymorphic: true
5
sangyongjung

Aucune des réponses précédentes ne m'a aidé à résoudre ce problème, je vais donc mettre cela ici au cas où quelqu'un d'autre se heurterait à cela. Utilisation de Rails 4.2 +.

Créez la migration (en supposant que vous disposez déjà d'une table Adresses):

class AddPolymorphicColumnsToAddress < ActiveRecord::Migration
  def change
    add_column :addresses, :addressable_type, :string, index: true
    add_column :addresses, :addressable_id, :integer, index: true
    add_column :addresses, :addressable_scope, :string, index: true
  end
end

Configurez votre association polymorphe:

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
end

Configurez la classe d'où l'association sera appelée:

class Order < ActiveRecord::Base
  has_one :bill_address, -> { where(addressable_scope: :bill_address) }, as: :addressable,  class_name: "Address", dependent: :destroy
  accepts_nested_attributes_for :bill_address, allow_destroy: true

  has_one :ship_address, -> { where(addressable_scope: :ship_address) }, as: :addressable, class_name: "Address", dependent: :destroy
  accepts_nested_attributes_for :ship_address, allow_destroy: true
end

L'astuce est que vous devez appeler la méthode de génération sur l'instance Order ou la colonne scope ne sera pas remplie.

Donc cela ne fonctionne PAS:

address = {attr1: "value"... etc...}
order = Order.new(bill_address: address)
order.save!

Cependant, cela FONCTIONNE.

address = {attr1: "value"... etc...}
order = Order.new
order.build_bill_address(address)
order.save!

J'espère que cela aide quelqu'un d'autre.

4
PR Whitehead

Je ne l'ai pas utilisé, mais j'ai fait une recherche sur Google et j'ai examiné les sources de Rails et je pense que ce que vous recherchez est :foreign_type. Essayez-le et dites si cela fonctionne :)

has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy, :foreign_type => 'SecondaryPost'

Je pense que le type de votre question devrait être Post au lieu de Photo et, respectivement, il serait préférable d'utiliser SecondaryPost comme il est attribué au modèle Post .

ÉDITER:

La réponse ci-dessus est complètement fausse. :foreign_type est disponible en modèle polymorphe dans belongs_to association pour spécifier le nom de la colonne qui contient le type de modèle associé.

Comme je regarde dans Rails sources, cette ligne définit ce type d'association:

dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]

Comme vous pouvez le voir, il utilise base_class.name pour obtenir le nom du type. Pour autant que je sache, vous ne pouvez rien en faire.

Donc, ma suggestion est d'ajouter une colonne au modèle Photo, par exemple: photo_type. Et définissez-le sur 0 s'il s'agit de la première photo, ou définissez-le sur 1 s'il s'agit de la deuxième photo. Dans vos associations, ajoutez :conditions => {:photo_type => 0} et :conditions => {:photo_type => 1}, respectivement. Je sais que ce n'est pas une solution que vous recherchez, mais je ne trouve rien de mieux. Soit dit en passant, il serait peut-être préférable d'utiliser simplement has_many association?

3
klew

Vous allez devoir patcher la notion de type_étranger dans la relation has_one. C'est ce que j'ai fait pour has_many. Dans un nouveau fichier .rb dans votre dossier d'initialiseurs, j'ai appelé le mien add_foreign_type_support.rb Il vous permet de spécifier quel doit être votre attachable_type. Exemple: has_many photo,: class_name => "Picture",: as => attachable,: foreign_type => 'Pic'

module ActiveRecord
  module Associations
    class HasManyAssociation < AssociationCollection #:nodoc:
      protected
        def construct_sql
          case
            when @reflection.options[:Finder_sql]
              @Finder_sql = interpolate_sql(@reflection.options[:Finder_sql])
           when @reflection.options[:as]
              resource_type = @reflection.options[:foreign_type].to_s.camelize || @owner.class.base_class.name.to_s
              @Finder_sql =  "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND "
              @Finder_sql += "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(resource_type)}"
              else
                @Finder_sql += ")"
              end
              @Finder_sql << " AND (#{conditions})" if conditions

            else
              @Finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
              @Finder_sql << " AND (#{conditions})" if conditions
          end

          if @reflection.options[:counter_sql]
            @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
          elsif @reflection.options[:Finder_sql]
            # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
            @reflection.options[:counter_sql] = @reflection.options[:Finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
            @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
          else
            @counter_sql = @Finder_sql
          end
        end
    end
  end
end
# Add foreign_type to options list
module ActiveRecord
  module Associations # :nodoc:
     module ClassMethods
      private
        mattr_accessor :valid_keys_for_has_many_association
        @@valid_keys_for_has_many_association = [
          :class_name, :table_name, :foreign_key, :primary_key, 
          :dependent,
          :select, :conditions, :include, :order, :group, :having, :limit, :offset,
          :as, :foreign_type, :through, :source, :source_type,
          :uniq,
          :Finder_sql, :counter_sql,
          :before_add, :after_add, :before_remove, :after_remove,
          :extend, :readonly,
          :validate, :inverse_of
        ]

    end
  end
2
simonslaw

Pour mongoid utilisez cette solution

A eu des moments difficiles après avoir découvert ce problème, mais a obtenu une solution intéressante qui fonctionne

Ajouter à votre Gemfile

gemme 'mongoid-multiple-polymorphic'

Et cela fonctionne comme un charme:

  class Resource

  has_one :icon, as: :assetable, class_name: 'Asset', dependent: :destroy, autosave: true
  has_one :preview, as: :assetable, class_name: 'Asset', dependent: :destroy, autosave: true

  end
1
comonitos

Aucune de ces solutions ne semble fonctionner sur Rails 5. Pour une raison quelconque, il semble que le comportement autour des conditions d'association ait changé. Lors de l'affectation de l'objet associé, les conditions ne semblent pas être utilisé dans l'encart; uniquement lors de la lecture de l'association.

Ma solution a été de remplacer la méthode setter pour l'association:

has_one :photo, -> { photo_type: 'primary_photo'},
        as: 'attachable',
        dependent: :destroy

def photo=(photo)
  photo.photo_type = 'primary_photo'
  super
end
1
Chris Edwards

Pouvez-vous ajouter un modèle SecondaryPhoto comme:

class SecondaryPhoto < Photo
end

puis ignorez le: nom_classe du has_one: photo_secondaire?

0
Rob Biedenharn