views:

825

answers:

4

My question is essentially the same as this one: http://stackoverflow.com/questions/1168047/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, "SecondaryPhoto" as I'd like...

Thoughts? Thanks!

+1  A: 

Can you add a SecondaryPhoto model like:

class SecondaryPhoto < Photo
end

and then skip the :class_name from the has_one :secondary_photo?

Rob Biedenharn
That does seem like a workable solution - I was hoping there was a "built-in" way to do what I wanted....
Matt Rogish
I tried that - it passes Photo as the type just as if I did class_name!
Matt Rogish
How are you "attaching" the secondary photo to the Post?In any case, I'm curious to know if klew's answer about :foreign_type works.
Rob Biedenharn
In `:secondary_photo` `:class_name` should be set to `Photo` since it is all about associated model. Problem here is with type field in polymorphic association and it isn't set by `:class_name`.
klew
+2  A: 

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?

klew
Unfortunately Rails 2.1 doesn't have foreign_type
Matt Rogish
I think it was added around 2.3. So I think that there is no other way of doing what you want. You can try adding this feature manualy or, what is much better, upgrade your application to 2.3.5 and always stay on latest version.
klew
I tried w/Rails 2.3.5 and it still tells me unknown key: foreign_type :(
Matt Rogish
Take a look at associations.rb valid_keys_for_has_one_association :class_name, :foreign_key, :remote, :select, :conditions, :order,:include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key
Matt Rogish
+1  A: 

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
simonslaw
ain't working when corrected syntax errors
sharas
This is the code I'm using with my rails 2.3.8 application. I would say step threw the logic and see if you have to make adjustments. The idea is your pass a foreign_type options params and if that exists you overload the default behavor on the select statement you might have to to_s.camelize the value if you pass it in as a :symbol
simonslaw
+3  A: 

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.

hakunin
I like the idea a lot, only problem is I can't get rails to write the correct value into the photo_type column, it's always Photo. Am I missing something, how do you get rails to write 'primary photo' into the photo_type column?
opsb
What's the name of your column? Make sure it's not just `type` since that's a special column rails uses to write the name of current class.
hakunin