Polymorphic Rails Association with Multiple Associations on the Same Model

My question is essentially the same as this one: Polymorphic association with multiple associations on the same model

However, the proposed / adopted decision does not work, as illustrated by the commentator later.

I have a Photo class that is used throughout my application. A message can have one photo. However, I want to reuse the polymorphic relation to add a secondary photo.

Before:

class Photo belongs_to :attachable, :polymorphic => true end class Post has_one :photo, :as => :attachable, :dependent => :destroy end 

Desired:

 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 

However, this fails because it cannot find the "SecondaryPhoto" class. Based on what I could say from this other topic, I would like to:

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

In addition to calling Post # secondary_photo simply returns the same photo that is attached via the photo association, for example. Message # photo === Message # secondary_photo. Looking at SQL, it does WHERE type = "Photo" instead of, say, "SecondaryPhoto" as I would like ...

Thoughts? Thank!

+46
ruby-on-rails polymorphic-associations
Mar 22 '10 at 17:41
source share
10 answers

I did this in my project.

The trick is that for photos you need a column that will be used in the has_one state to distinguish between primary and secondary photos. Pay attention to what happens in :conditions here.

 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 

The beauty of this approach is that when creating photos using @post.build_photo , the photo_type file will be automatically filled with the appropriate type, for example, 'primary_photo'. ActiveRecord is smart enough to do this.

+63
Jun 20 '10 at 5:05
source share

Rails 4.2+

 class Photo belongs_to :attachable, :polymorphic => true end class Post has_one :photo, :as => :attachable, :dependent => :destroy has_one :secondary_photo, -> { where attachable_type: "SecondaryPhoto"}, class_name: Photo, foreign_key: :attachable_id, foreign_type: :attachable_type, dependent: :destroy end 

You need to specify foreign_key according to .... able'ness or Rails will request the post_id column in the photo table. Attachable_type column fills Rails magic as SecondaryPhoto

+18
Feb 01 '15 at 7:37
source share

Future link for people checking this post.

This can be achieved with the following code ...

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 

Not sure why, but in Rails 3 you need to specify the foreign_key value along with the conditions and class_name. Do not use 'as :: attachable', as this will automatically use the name of the calling class when setting up the polymorphic type.

The above also applies to has_many.

+4
Oct 13 '14 at 2:06
source share

Something like the following worked for requests, but assignment from user to address did not work

Custom class

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

Class address

 belongs_to :address_holder, polymorphic: true 
+3
Oct 22 '14 at 17:31
source share

I did not use it, but I searched googled and studied the sources of Rails, and I think you are looking for :foreign_type . Try it and tell me if it works :)

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

I think that this type in your question should be Post instead of Photo and, accordingly, it would be better to use SecondaryPost , since it is assigned to the Post model.

EDIT:

Above answer is completely wrong. :foreign_type is available in the polymorphic model in the belongs_to association to specify the name of the column containing the type of the associated model.

As I see in Rails sources, this line sets this type for association:

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

As you can see, it uses base_class.name to get the type name. As far as I know, there is nothing you can do about it.

So my suggestion is to add one column to the Photo Model, for example: photo_type . And set it to 0 if it is the first photo, or set it to 1 if it is the second photo. In your associations, add :conditions => {:photo_type => 0} and :conditions => {:photo_type => 1} , respectively. I know that this is not the solution you are looking for, but I cannot find anything better. By the way, maybe it's better to just use the has_many association?

+2
Mar 22
source share

You will have the opportunity for the monkey to correct the concept of foreign_type in has_one. This is what I did for has_many. In the new .rb file in the initialization folder, I named my add_foreign_type_support.rb. It allows you to specify what your attachable_type will be. Example: 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
Jun 10 '10 at 6:39
source share

None of the previous answers helped me solve this problem, so I will put this here if anyone else comes across this. Using Rails 4.2 +.

Create a migration (if you already have an address table):

 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 

Setting up your polymorphic association:

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

Set the class from which the link will be called:

 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 

The trick is that you have to call the assembly method in the Order instance or the scope column will not be filled.

So this does NOT work:

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

However, it WORKS.

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

Hope this helps someone else.

+2
Apr 04 '17 at 11:26 on
source share

Can you add a SecondaryPhoto model, for example:

 class SecondaryPhoto < Photo end 

and then skip: class_name from has_one: secondary_photo?

+1
Mar 22 '10 at 20:19
source share

For mongoid use this solution

In difficult times, after discovering this problem, but it turned out a cool solution that works

Add to Your Gemfile

gem 'mongoid-multiple-polymorphic'

And it works like a charm:

  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
Jun 14 '15 at 15:07
source share

None of these solutions seem to work on Rails 5. For some reason, the behavior of the association conditions seems to have changed. When assigning a related object, the conditions do not appear to be used in the insert; only when reading the association.

My solution was to override the setter method for the association:

 has_one :photo, -> { photo_type: 'primary_photo'}, as: 'attachable', dependent: :destroy def photo=(photo) photo.photo_type = 'primary_photo' super end 
0
Oct 26 '17 at 9:00
source share



All Articles