Rails Polymorphic 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/accepted solution does not work, as illustrated by a commenter later.
I have a Photo class that is used all over my app. A post can have a single photo. However, I want to re-use the polymorphic relationship 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 as it cannot find the class "SecondaryPhoto". Based on what I could tell from that other thread, I'd want to do:
has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy
Except calling Post#secondary_photo simply returns the same photo that is attached via the Photo association, e.g. Post#photo === Post#secondary_photo. Looking at the SQL, it does WHERE type = "Photo" instead of, say, "SecondaryPhot开发者_JS百科o" as I'd like...
Thoughts? Thanks!
I have done that in my project.
The trick is that photos need a column that will be used in has_one condition 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 you create photos using @post.build_photo
, the photo_type will automatically be pre-populated with corresponding type, like 'primary_photo'. ActiveRecord is smart enough to do that.
In Rails 5 you have to define attr_accessor for :attachable_id and specify for relation :class_name and :foreign_key options only. You will get ...AND attachable_type = 'SecondaryPhoto' if as: :attachable used
class Post
attr_accessor :attachable_id
has_one :photo, :as => :attachable, :dependent => :destroy
has_one :secondary_photo, -> { where attachable_type: 'SecondaryPhoto' }, class_name: "Photo", dependent: :destroy, foreign_key: :attachable_id
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 provide foreign_key according ....able'ness or Rails will ask for post_id column in photo table. Attachable_type column will fills with Rails magic as SecondaryPhoto
None of the previous answers helped me solve this problem, so I'll put this here incase anyone else runs into this. Using Rails 4.2 +.
Create the migration (assuming you have an Addresses table already):
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
Setup your polymorphic association:
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
end
Setup the class where the association will be called from:
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 build method on the Order
instance or the scope
column won't be populated.
So this does NOT work:
address = {attr1: "value"... etc...}
order = Order.new(bill_address: address)
order.save!
However, this DOES WORK.
address = {attr1: "value"... etc...}
order = Order.new
order.build_bill_address(address)
order.save!
Hope that helps someone else.
Something like following worked for querying, but assigning from User to address didn't work
User Class
has_many :addresses, as: :address_holder
has_many :delivery_addresses, -> { where :address_holder_type => "UserDelivery" },
class_name: "Address", foreign_key: "address_holder_id"
Address Class
belongs_to :address_holder, polymorphic: true
Future reference for people checking this post
This can be achieved using 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 supply a foreign_key value alongside the conditions and class_name. Do not use 'as: :attachable' as this will automatically use the calling class name when setting the polymorphic type.
The above applies to has_many too.
I didn't use it, but I googled around and looked into Rails sources and I think that what you're looking for is :foreign_type
. Try it and tell if it works :)
has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy, :foreign_type => 'SecondaryPost'
I think that type in your question should be Post
instead of Photo
and, respectively, it would be better to use SecondaryPost
as it assigned to Post
model.
EDIT:
Above answer is completly wrong. :foreign_type
is availble in polymorphic model in belongs_to
association to specify name of the column that contains type of associated model.
As I look 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 type name. As far as I know you can do nothing with it.
So my sugestion is to add one column to Photo model, on example: photo_type
. And set it to 0 if it is first photo, or set it to 1 if it is second photo. In your associations add :conditions => {:photo_type => 0}
and :conditions => {:photo_type => 1}
, respectively. I know it is not a solution you are looking for, but I can't find anything better. By the way, maybe it would be better to just use has_many
association?
Your going to have to monkey patch the notion of foreign_type into has_one relationship. This is what i did for has_many. In a new .rb file in your initializers folder i called mine add_foreign_type_support.rb It lets you specify what your attachable_type is to 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
None of these solutions seem to work on Rails 5. For some reason, it looks like the behaviour around the association conditions has changed. When assigning the related object, the conditions don't seem 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
For mongoid use this solution
Had tough times after discovering this issue but got cool solution that works
Add to your Gemfile
gem 'mongoid-multiple-polymorphic'
And this 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
Might be a bit late, but this might help someone so here is how I fix this (rails 5.2
, ruby 2.6
):
I added an enum
, called kind
to the model and then added the proper scope to the has_one
association:
class Photo
belongs_to :attachable, :polymorphic => true
enum kind: %i[first_photo secondary_photo]
end
class Post
has_one :photo, -> { where(kind: :first_photo) }, :as => :attachable, :dependent => :destroy
has_one :secondary_photo, -> { where(kind: :secondary_photo) }, :as => :attachable, :dependent => :destroy
end
The scope is needed because ActiveRecord can discriminate between the objects/association.
Hope the above helps!
精彩评论