Class methods added to ActiveRecord::Base for setting up polymorphic associations.
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.
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.
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 59 def acts_as_double_polymorphic_join options={}, &extension collections, options = extract_double_collections(options) # handle the block options[:extend] = (if options[:extend] Array(options[:extend]) + [extension] else extension end) if extension collection_option_keys = make_general_option_keys_specific!(options, collections) join_name = self.name.tableize.to_sym collections.each do |association_id, children| parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array begin parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name rescue NoMethodError raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key end parents = collections[parent_hash_key] conflicts = (children & parents) # set intersection parents.each do |plural_parent_name| parent_class = plural_parent_name._as_class singular_reverse_association_id = parent_hash_key._singularize internal_options = { :is_double => true, :from => children, :as => singular_reverse_association_id, :through => join_name.to_sym, :foreign_key => parent_foreign_key, :foreign_type_key => parent_foreign_key.to_s.sub(%r_id$/, '_type'), :singular_reverse_association_id => singular_reverse_association_id, :conflicts => conflicts } general_options = Hash[*options._select do |key, value| collection_option_keys[association_id].include? key and !value.nil? end.map do |key, value| [key.to_s[association_id.to_s.length+1..-1].to_sym, value] end._flatten_once] # rename side-specific options to general names general_options.each do |key, value| # avoid clobbering keys that appear in both option sets if internal_options[key] general_options[key] = Array(value) + Array(internal_options[key]) end end parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options)) if conflicts.include? plural_parent_name # unify the alternate sides of the conflicting children (conflicts).each do |method_name| unless parent_class.instance_methods.include?(method_name) parent_class.send(:define_method, method_name) do (self.send("#{singular_reverse_association_id}_#{method_name}") + self.send("#{association_id._singularize}_#{method_name}")).freeze end end end # unify the join model... join model is always renamed for doubles, unlike child associations unless parent_class.instance_methods.include?(join_name) parent_class.send(:define_method, join_name) do (self.send("#{join_name}_as_#{singular_reverse_association_id}") + self.send("#{join_name}_as_#{association_id._singularize}")).freeze end end else unless parent_class.instance_methods.include?(join_name) # ensure there are no forward slashes in the aliased join_name_method (occurs when namespaces are used) join_name_method = join_name.to_s.gsub('/', '_').to_sym parent_class.send(:alias_method, join_name_method, "#{join_name_method}_as_#{singular_reverse_association_id}") end end end end end
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.)
: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
.
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 191 def has_many_polymorphs(association_id, options = {}, &extension) _logger_debug "associating #{self}.#{association_id}" reflection = create_has_many_polymorphs_reflection(association_id, options, &extension) # puts "Created reflection #{reflection.inspect}" # configure_dependency_for_has_many(reflection) collection_reader_method(reflection, PolymorphicAssociation) end