Module: ActiveRecord::Associations::PolymorphicClassMethods

Class methods added to ActiveRecord::Base for setting up polymorphic associations.

Notes

STI association targets must enumerated and named. For example, if Dog and Cat both inherit from Animal, you still need to say [:dogs, :cats], and not [:animals].

Namespaced models follow the Rails underscore convention. ZooAnimal::Lion becomes :’zoo_animal/lion‘.

You do not need to set up any other associations other than for either the regular method or the double. The join associations and all individual and reverse associations are generated for you. However, a join model and table are required.

There is a tentative report that you can make the parent model be its own join model, but this is untested.

Public Instance Methods


acts_as_double_polymorphic_join (options={})

This method creates a doubled-sided polymorphic relationship. It must be called on the join model:

  class Devouring < ActiveRecord::Base
    belongs_to :eater, :polymorphic => true
    belongs_to :eaten, :polymorphic => true

    acts_as_double_polymorphic_join(
      :eaters => [:dogs, :cats],
      :eatens => [:cats, :birds]
    )
  end

The method works by defining one or more special has_many_polymorphs association on every model in the target lists, depending on which side of the association it is on. Double self-references will work.

The two association names and their value arrays are the only required parameters.

Available options

These options are passed through to targets on both sides of the association. If you want to affect only one side, prepend the key with the name of that side. For example, :eaters_extend.

:dependent:Accepts :destroy, :nullify, or :delete_all. Controls how the join record gets treated on any association delete (whether from the polymorph or from an individual collection); defaults to :destroy.
:skip_duplicates:If true, will check to avoid pushing already associated records (but also triggering a database load). Defaults to true.
:rename_individual_collections:If true, all individual collections are prepended with the polymorph name, and the children‘s parent collection is appended with "_of_#{association_name}".
:extend:One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
:join_extend:One or an array of mixed modules and procs, which are applied to the join association.
:conditions:An array or string of conditions for the SQL WHERE clause.
:order:A string for the SQL ORDER BY clause.
:limit:An integer. Affects the polymorphic and individual associations.
:offset:An integer. Only affects the polymorphic association.
:namespace:A symbol. Prepended to all the models in the :from and :through keys. This is especially useful for Camping, which namespaces models by default.
     # File lib/has_many_polymorphs/class_methods.rb, line 62
 62:       def acts_as_double_polymorphic_join options={}, &extension      
 63:         
 64:         collections, options = extract_double_collections(options)
 65:         
 66:         # handle the block
 67:         options[:extend] = (if options[:extend]
 68:           Array(options[:extend]) + [extension]
 69:         else 
 70:           extension
 71:         end) if extension 
 72:         
 73:         collection_option_keys = make_general_option_keys_specific!(options, collections)
 74:   
 75:         join_name = self.name.tableize.to_sym
 76:         collections.each do |association_id, children|
 77:           parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array
 78:           
 79:           begin
 80:             parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
 81:           rescue NoMethodError
 82:             raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key
 83:           end
 84: 
 85:           parents = collections[parent_hash_key]
 86:           conflicts = (children & parents) # set intersection          
 87:           parents.each do |plural_parent_name| 
 88:   
 89:             parent_class = plural_parent_name._as_class
 90:             singular_reverse_association_id = parent_hash_key._singularize 
 91:               
 92:             internal_options = {
 93:               :is_double => true,
 94:               :from => children, 
 95:               :as => singular_reverse_association_id,
 96:               :through => join_name.to_sym, 
 97:               :foreign_key => parent_foreign_key, 
 98:               :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
 99:               :singular_reverse_association_id => singular_reverse_association_id,
100:               :conflicts => conflicts
101:             }
102:             
103:             general_options = Hash[*options._select do |key, value|
104:               collection_option_keys[association_id].include? key and !value.nil?
105:             end.map do |key, value|
106:               [key.to_s[association_id.to_s.length+1..-1].to_sym, value]
107:             end._flatten_once] # rename side-specific options to general names
108:             
109:             general_options.each do |key, value|
110:               # avoid clobbering keys that appear in both option sets
111:               if internal_options[key]
112:                 general_options[key] = Array(value) + Array(internal_options[key])
113:               end
114:             end
115: 
116:             parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
117:   
118:             if conflicts.include? plural_parent_name 
119:               # unify the alternate sides of the conflicting children
120:               (conflicts).each do |method_name|
121:                 unless parent_class.instance_methods.include?(method_name)
122:                   parent_class.send(:define_method, method_name) do
123:                     (self.send("#{singular_reverse_association_id}_#{method_name}") + 
124:                       self.send("#{association_id._singularize}_#{method_name}")).freeze
125:                   end
126:                 end     
127:               end            
128:               
129:               # unify the join model... join model is always renamed for doubles, unlike child associations
130:               unless parent_class.instance_methods.include?(join_name)
131:                 parent_class.send(:define_method, join_name) do
132:                   (self.send("#{join_name}_as_#{singular_reverse_association_id}") + 
133:                     self.send("#{join_name}_as_#{association_id._singularize}")).freeze
134:                 end              
135:               end                         
136:             else
137:               unless parent_class.instance_methods.include?(join_name)
138:                 parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}")
139:               end
140:             end                      
141:   
142:           end
143:         end
144:       end

has_many_polymorphs (association_id, options = {}, &extension)

This method createds a single-sided polymorphic relationship.

  class Petfood < ActiveRecord::Base
    has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
  end

The only required parameter, aside from the association name, is :from.

The method generates a number of associations aside from the polymorphic one. In this example Petfood also gets dogs, cats, and birds, and Dog, Cat, and Bird get petfoods. (The reverse association to the parents is always plural.)

Available options

:from:An array of symbols representing the target models. Required.
:as:A symbol for the parent‘s interface in the join—what the parent ‘acts as’.
:through:A symbol representing the class of the join model. Follows Rails defaults if not supplied (the parent and the association names, alphabetized, concatenated with an underscore, and singularized).
:dependent:Accepts :destroy, :nullify, :delete_all. Controls how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to :destroy.
:skip_duplicates:If true, will check to avoid pushing already associated records (but also triggering a database load). Defaults to true.
:rename_individual_collections:If true, all individual collections are prepended with the polymorph name, and the children‘s parent collection is appended with "of#{association_name}"</tt>. For example, zoos becomes zoos_of_animals. This is to help avoid method name collisions in crowded classes.
:extend:One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
:join_extend:One or an array of mixed modules and procs, which are applied to the join association.
:parent_extend:One or an array of mixed modules and procs, which are applied to the target models’ association to the parents.
:conditions:An array or string of conditions for the SQL WHERE clause.
:parent_conditions:An array or string of conditions which are applied to the target models’ association to the parents.
:order:A string for the SQL ORDER BY clause.
:parent_order:A string for the SQL ORDER BY which is applied to the target models’ association to the parents.
:group:An array or string of conditions for the SQL GROUP BY clause. Affects the polymorphic and individual associations.
:limit:An integer. Affects the polymorphic and individual associations.
:offset:An integer. Only affects the polymorphic association.
:namespace:A symbol. Prepended to all the models in the :from and :through keys. This is especially useful for Camping, which namespaces models by default.
:uniq:If true, the records returned are passed through a pure-Ruby uniq before they are returned. Rarely needed.
:foreign_key:The column name for the parent‘s id in the join.
:foreign_type_key:The column name for the parent‘s class name in the join, if the parent itself is polymorphic. Rarely needed—if you‘re thinking about using this, you almost certainly want to use acts_as_double_polymorphic_join() instead.
:polymorphic_key:The column name for the child‘s id in the join.
:polymorphic_type_key:The column name for the child‘s class name in the join.

If you pass a block, it gets converted to a Proc and added to :extend.

On condition nullification

When you request an individual association, non-applicable but fully-qualified fields in the polymorphic association‘s :conditions, :order, and :group options get changed to NULL. For example, if you set :conditions => "dogs.name != ‘Spot’", when you request .cats, the conditions string is changed to NULL != ‘Spot‘.

Be aware, however, that NULL != ‘Spot‘ returns false due to SQL‘s 3-value logic. Instead, you need to use the :conditions string "dogs.name IS NULL OR dogs.name != ‘Spot’" to get the behavior you probably expect for negative matches.

     # File lib/has_many_polymorphs/class_methods.rb, line 241
241:       def has_many_polymorphs (association_id, options = {}, &extension)
242:         _logger_debug "associating #{self}.#{association_id}"
243:         reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
244:         # puts "Created reflection #{reflection.inspect}"
245:         # configure_dependency_for_has_many(reflection)
246:         collection_reader_method(reflection, PolymorphicAssociation)
247:       end