2 module ActiveRecord #:nodoc:
3 module Associations #:nodoc:
7 Class methods added to ActiveRecord::Base for setting up polymorphic associations.
11 STI association targets must enumerated and named. For example, if Dog and Cat both inherit from Animal, you still need to say <tt>[:dogs, :cats]</tt>, and not <tt>[:animals]</tt>.
13 Namespaced models follow the Rails <tt>underscore</tt> convention. ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
15 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.
17 There is a tentative report that you can make the parent model be its own join model, but this is untested.
21 module PolymorphicClassMethods
23 RESERVED_DOUBLES_KEYS = [:conditions, :order, :limit, :offset, :extend, :skip_duplicates,
24 :join_extend, :dependent, :rename_individual_collections,
29 This method creates a doubled-sided polymorphic relationship. It must be called on the join model:
31 class Devouring < ActiveRecord::Base
32 belongs_to :eater, :polymorphic => true
33 belongs_to :eaten, :polymorphic => true
35 acts_as_double_polymorphic_join(
36 :eaters => [:dogs, :cats],
37 :eatens => [:cats, :birds]
41 The method works by defining one or more special <tt>has_many_polymorphs</tt> association on every model in the target lists, depending on which side of the association it is on. Double self-references will work.
43 The two association names and their value arrays are the only required parameters.
47 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, <tt>:eaters_extend</tt>.
49 <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, or <tt>:delete_all</tt>. Controls how the join record gets treated on any association delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
50 <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
51 <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with <tt>"\_of_#{association_name}"</tt>.
52 <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
53 <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
54 <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
55 <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
56 <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
57 <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
58 <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
62 def acts_as_double_polymorphic_join options={}, &extension
64 collections, options = extract_double_collections(options)
67 options[:extend] = (if options[:extend]
68 Array(options[:extend]) + [extension]
73 collection_option_keys = make_general_option_keys_specific!(options, collections)
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
80 parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
82 raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key
85 parents = collections[parent_hash_key]
86 conflicts = (children & parents) # set intersection
87 parents.each do |plural_parent_name|
89 parent_class = plural_parent_name._as_class
90 singular_reverse_association_id = parent_hash_key._singularize
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
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
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])
116 parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
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
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
137 unless parent_class.instance_methods.include?(join_name)
138 parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}")
148 def extract_double_collections(options)
149 collections = options._select do |key, value|
150 value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
153 raise PolymorphicError, "Couldn't understand options in acts_as_double_polymorphic_join. Valid parameters are your two class collections, and then #{RESERVED_DOUBLES_KEYS.inspect[1..-2]}, with optionally your collection names prepended and joined with an underscore." unless collections.size == 2
155 options = options._select do |key, value|
159 [collections, options]
162 def make_general_option_keys_specific!(options, collections)
163 collection_option_keys = Hash[*collections.keys.map do |key|
164 [key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}]
167 collections.keys.each do |collection|
168 options.each do |key, value|
169 next if collection_option_keys.values.flatten.include? key
170 # shift the general options to the individual sides
171 collection_key = "#{collection}_#{key}".to_sym
172 collection_value = options[collection_key]
175 collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
176 options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
178 options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
179 when :extend, :join_extend
180 options[collection_key] = Array(collection_value) + Array(value)
182 options[collection_key] ||= value
187 collection_option_keys
196 This method createds a single-sided polymorphic relationship.
198 class Petfood < ActiveRecord::Base
199 has_many_polymorphs :eaters, :from => [:dogs, :cats, :birds]
202 The only required parameter, aside from the association name, is <tt>:from</tt>.
204 The method generates a number of associations aside from the polymorphic one. In this example Petfood also gets <tt>dogs</tt>, <tt>cats</tt>, and <tt>birds</tt>, and Dog, Cat, and Bird get <tt>petfoods</tt>. (The reverse association to the parents is always plural.)
208 <tt>:from</tt>:: An array of symbols representing the target models. Required.
209 <tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
210 <tt>:through</tt>:: 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).
211 <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, <tt>:delete_all</tt>. Controls how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
212 <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
213 <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with "_of_#{association_name}"</tt>. For example, <tt>zoos</tt> becomes <tt>zoos_of_animals</tt>. This is to help avoid method name collisions in crowded classes.
214 <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
215 <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
216 <tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
217 <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
218 <tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
219 <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
220 <tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
221 <tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
222 <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
223 <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
224 <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
225 <tt>:uniq</tt>:: If <tt>true</tt>, the records returned are passed through a pure-Ruby <tt>uniq</tt> before they are returned. Rarely needed.
226 <tt>:foreign_key</tt>:: The column name for the parent's id in the join.
227 <tt>:foreign_type_key</tt>:: 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 <tt>acts_as_double_polymorphic_join()</tt> instead.
228 <tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
229 <tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
231 If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>.
233 == On condition nullification
235 When you request an individual association, non-applicable but fully-qualified fields in the polymorphic association's <tt>:conditions</tt>, <tt>:order</tt>, and <tt>:group</tt> options get changed to <tt>NULL</tt>. For example, if you set <tt>:conditions => "dogs.name != 'Spot'"</tt>, when you request <tt>.cats</tt>, the conditions string is changed to <tt>NULL != 'Spot'</tt>.
237 Be aware, however, that <tt>NULL != 'Spot'</tt> returns <tt>false</tt> due to SQL's 3-value logic. Instead, you need to use the <tt>:conditions</tt> string <tt>"dogs.name IS NULL OR dogs.name != 'Spot'"</tt> to get the behavior you probably expect for negative matches.
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)
249 # Composed method that assigns option defaults, builds the reflection object, and sets up all the related associations on the parent, join, and targets.
250 def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
251 options.assert_valid_keys(
257 :polymorphic_key, # same as :association_foreign_key
258 :polymorphic_type_key,
259 :dependent, # default :destroy, only affects the join table
260 :skip_duplicates, # default true, only affects the polymorphic collection
261 :ignore_duplicates, # deprecated
263 :rename_individual_collections,
264 :reverse_association_id, # not used
265 :singular_reverse_association_id,
272 :select, # applies to the polymorphic relationship
273 :conditions, # applies to the polymorphic relationship, the children, and the join
277 :order, # applies to the polymorphic relationship, the children, and the join
278 :group, # only applies to the polymorphic relationship and the children
279 :limit, # only applies to the polymorphic relationship and the children
280 :offset, # only applies to the polymorphic relationship
287 :uniq, # XXX untested, only applies to the polymorphic relationship
296 # validate against the most frequent configuration mistakes
297 verify_pluralization_of(association_id)
298 raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array
299 options[:from].each{|plural| verify_pluralization_of(plural)}
301 options[:as] ||= self.name.demodulize.underscore.to_sym
302 options[:conflicts] = Array(options[:conflicts])
303 options[:foreign_key] ||= "#{options[:as]}_id"
305 options[:association_foreign_key] =
306 options[:polymorphic_key] ||= "#{association_id._singularize}_id"
307 options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
309 if options.has_key? :ignore_duplicates
310 _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
311 options[:skip_duplicates] = options[:ignore_duplicates]
313 options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
314 options[:dependent] = :destroy unless options.has_key? :dependent
315 options[:conditions] = sanitize_sql(options[:conditions])
317 # options[:finder_sql] ||= "(options[:polymorphic_key]
319 options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
321 # set up namespaces if we have a namespace key
322 # XXX needs test coverage
323 if options[:namespace]
324 namespace = options[:namespace].to_s.chomp("/") + "/"
325 options[:from].map! do |child|
326 "#{namespace}#{child}".to_sym
328 options[:through] = "#{namespace}#{options[:through]}".to_sym
331 options[:join_class_name] ||= options[:through]._classify
332 options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
333 options[:select] ||= build_select(association_id, options[:table_aliases])
335 options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
336 options[:through] = demodulate(options[:through]).to_sym
338 options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
339 options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
340 options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")
342 # create the reflection object
343 returning(create_reflection(:has_many_polymorphs, association_id, options, self)) do |reflection|
344 if defined? Dependencies and defined? RAILS_ENV and RAILS_ENV == "development"
345 inject_dependencies(association_id, reflection) if Dependencies.mechanism == :load
348 # set up the other related associations
349 create_join_association(association_id, reflection)
350 create_has_many_through_associations_for_parent_to_children(association_id, reflection)
351 create_has_many_through_associations_for_children_to_parent(association_id, reflection)
358 # table mapping for use at the instantiation point
360 def build_table_aliases(from)
362 returning({}) do |aliases|
363 from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
365 table = plural._as_class.table_name
366 rescue NameError => e
367 raise PolymorphicError, "Could not find a valid class for #{plural.inspect}. If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead."
369 plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
370 aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
376 def build_select(association_id, aliases)
377 # <tt>instantiate</tt> has to know which reflection the results are coming from
378 (["\'#{self.name}\' AS polymorphic_parent_class",
379 "\'#{association_id}\' AS polymorphic_association_id"] +
380 aliases.map do |table, _alias|
381 "#{table} AS #{_alias}"
386 def inject_dependencies(association_id, reflection)
387 _logger_debug "injecting dependencies"
388 requirements = [self, reflection.klass].map{|klass| [klass, klass.base_class]}.flatten.uniq
389 (all_classes_for(association_id, reflection) - requirements).each do |target_klass|
390 Dependencies.inject_dependency(target_klass, *requirements)
394 # method sub-builders
396 def create_join_association(association_id, reflection)
399 :foreign_key => reflection.options[:foreign_key],
400 :dependent => reflection.options[:dependent],
401 :class_name => reflection.klass.name,
402 :extend => reflection.options[:join_extend]
403 # :limit => reflection.options[:limit],
404 # :offset => reflection.options[:offset],
405 # :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
406 # :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
409 if reflection.options[:foreign_type_key]
410 type_check = "#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
411 conjunction = options[:conditions] ? " AND " : nil
412 options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
413 options[:as] = reflection.options[:as]
416 has_many(reflection.options[:through], options)
418 inject_before_save_into_join_table(association_id, reflection)
421 def inject_before_save_into_join_table(association_id, reflection)
422 sti_hook = "sti_class_rewrite"
423 rewrite_procedure = %[self.send(:#{reflection.options[:polymorphic_type_key]}=, self.#{reflection.options[:polymorphic_type_key]}.constantize.base_class.name)]
425 # XXX should be abstracted?
426 reflection.klass.class_eval %[
427 unless instance_methods.include? "before_save_with_#{sti_hook}"
428 if instance_methods.include? "before_save"
429 alias_method :before_save_without_#{sti_hook}, :before_save
430 def before_save_with_#{sti_hook}
431 before_save_without_#{sti_hook}
435 def before_save_with_#{sti_hook}
439 alias_method :before_save, :before_save_with_#{sti_hook}
444 def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
446 child_pluralization_map(association_id, reflection).each do |plural, singular|
447 if singular == reflection.options[:as]
448 raise PolymorphicError, if reflection.options[:is_double]
449 "You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
451 "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
456 plural._as_class.instance_eval do
457 # this shouldn't be called at all during doubles; there is no way to traverse to a double polymorphic parent (XXX is that right?)
458 unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym
461 through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym
463 :as => association_id._singularize,
464 # :source => association_id._singularize,
465 # :source_type => reflection.options[:polymorphic_type_key],
466 :class_name => reflection.klass.name,
467 :dependent => reflection.options[:dependent],
468 :extend => reflection.options[:join_extend],
469 # :limit => reflection.options[:limit],
470 # :offset => reflection.options[:offset],
471 :order => devolve(association_id, reflection, reflection.options[:parent_order], reflection.klass),
472 :conditions => devolve(association_id, reflection, reflection.options[:parent_conditions], reflection.klass)
475 # the association to the target's parents
476 association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym
477 has_many(association,
479 :class_name => parent.name,
480 :source => reflection.options[:as],
481 :foreign_key => reflection.options[:foreign_key],
482 :extend => reflection.options[:parent_extend],
483 :conditions => reflection.options[:parent_conditions],
484 :order => reflection.options[:parent_order],
485 :offset => reflection.options[:parent_offset],
486 :limit => reflection.options[:parent_limit],
487 :group => reflection.options[:parent_group])
489 # debugger if association == :parents
498 def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
499 child_pluralization_map(association_id, reflection).each do |plural, singular|
500 #puts ":source => #{child}"
501 current_association = demodulate(child_association_map(association_id, reflection)[plural])
502 source = demodulate(singular)
504 if reflection.options[:conflicts].include? plural
506 current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
507 source = "#{source}_as_#{association_id._singularize}".to_sym
510 # make push/delete accessible from the individual collections but still operate via the general collection
511 extension_module = self.class_eval %[
512 module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
513 def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end
515 def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
516 def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
520 has_many(current_association.to_sym,
521 :through => reflection.options[:through],
522 :source => association_id._singularize,
523 :source_type => plural._as_class.base_class.name,
524 :class_name => plural._as_class.name, # make STI not conflate subtypes
525 :extend => (Array(extension_module) + reflection.options[:extend]),
526 :limit => reflection.options[:limit],
527 # :offset => reflection.options[:offset],
528 :order => devolve(association_id, reflection, reflection.options[:order], plural._as_class),
529 :conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class),
530 :group => devolve(association_id, reflection, reflection.options[:group], plural._as_class)
536 # some support methods
538 def child_pluralization_map(association_id, reflection)
539 Hash[*reflection.options[:from].map do |plural|
540 [plural, plural._singularize]
544 def child_association_map(association_id, reflection)
545 Hash[*reflection.options[:from].map do |plural|
546 [plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
551 s.to_s.gsub('/', '_').to_sym
554 def build_join_table_symbol(association_id, name)
555 [name.to_s, association_id.to_s].sort.join("_").to_sym
558 def all_classes_for(association_id, reflection)
559 klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
560 klasses += klasses.map(&:base_class)
564 def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false)
565 # XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
568 # _logger_debug "devolving #{string} for #{klass}"
569 inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
570 [klass, klass.base_class, reflection.klass, reflection.klass.base_class])
571 inappropriate_classes.map do |klass|
572 klass.columns.map do |column|
573 [klass.table_name, column.name]
574 end.map do |table, column|
575 ["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
577 end.flatten.sort_by(&:size).reverse.each do |quoted_reference|
578 # _logger_debug "devolved #{quoted_reference} to NULL"
579 # XXX clause removal would go here
580 string.gsub!(quoted_reference, "NULL")
582 # _logger_debug "altered to #{string}"
586 def verify_pluralization_of(sym)
588 singular = sym.singularize
589 plural = singular.pluralize
590 raise PolymorphicError, "Pluralization rules not set up correctly. You passed :#{sym}, which singularizes to :#{singular}, but that pluralizes to :#{plural}, which is different. Maybe you meant :#{plural} to begin with?" unless sym == plural
593 def spiked_create_extension_module(association_id, extensions, identifier = nil)
594 module_extensions = extensions.select{|e| e.is_a? Module}
595 proc_extensions = extensions.select{|e| e.is_a? Proc }
597 # support namespaced anonymous blocks as well as multiple procs
598 proc_extensions.each_with_index do |proc_extension, index|
599 module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
600 the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
601 the_module.class_eval &proc_extension
602 module_extensions << the_module