Module: ActiveRecord::Associations::PolymorphicClassMethods
- Included in:
- Base
- Defined in:
- lib/has_many_polymorphs/class_methods.rb
Overview
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.
Constant Summary collapse
- RESERVED_DOUBLES_KEYS =
[ :conditions, :order, :limit, :offset, :extend, :skip_duplicates, :join_extend, :dependent, :rename_individual_collections, :namespace ]
Instance Method Summary collapse
-
#acts_as_double_polymorphic_join(options = {}, &extension) ⇒ Object
This method creates a doubled-sided polymorphic relationship.
-
#create_has_many_polymorphs_reflection(association_id, options, &extension) ⇒ Object
Composed method that assigns option defaults, builds the reflection object, and sets up all the related associations on the parent, join, and targets.
-
#has_many_polymorphs(association_id, options = {}, &extension) ⇒ Object
This method createds a single-sided polymorphic relationship.
Instance Method Details
#acts_as_double_polymorphic_join(options = {}, &extension) ⇒ Object
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 totrue
. :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.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/has_many_polymorphs/class_methods.rb', line 63 def acts_as_double_polymorphic_join ={}, &extension collections, = extract_double_collections() # handle the block [:extend] = (if [:extend] Array([:extend]) + [extension] else extension end) if extension collection_option_keys = make_general_option_keys_specific!(, 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 unless parent_foreign_key msg = "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." raise PolymorphicError, msg end 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 = { :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(/_id$/, '_type'), :singular_reverse_association_id => singular_reverse_association_id, :conflicts => conflicts } = Hash[*._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 .each do |key, value| # avoid clobbering keys that appear in both option sets if [key] [key] = Array(value) + Array([key]) end end parent_class.send(:has_many_polymorphs, association_id, .merge()) 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 |
#create_has_many_polymorphs_reflection(association_id, options, &extension) ⇒ Object
Composed method that assigns option defaults, builds the reflection object, and sets up all the related associations on the parent, join, and targets.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/has_many_polymorphs/class_methods.rb', line 207 def create_has_many_polymorphs_reflection(association_id, , &extension) #:nodoc: .assert_valid_keys( :from, :as, :through, :foreign_key, :foreign_type_key, :polymorphic_key, # same as :association_foreign_key :polymorphic_type_key, :dependent, # default :destroy, only affects the join table :skip_duplicates, # default true, only affects the polymorphic collection :ignore_duplicates, # deprecated :is_double, :rename_individual_collections, :reverse_association_id, # not used :singular_reverse_association_id, :conflicts, :extend, :join_class_name, :join_extend, :parent_extend, :table_aliases, :select, # applies to the polymorphic relationship :conditions, # applies to the polymorphic relationship, the children, and the join # :include, :parent_conditions, :parent_order, :order, # applies to the polymorphic relationship, the children, and the join :group, # only applies to the polymorphic relationship and the children :limit, # only applies to the polymorphic relationship and the children :offset, # only applies to the polymorphic relationship :parent_order, :parent_group, :parent_limit, :parent_offset, # :source, :namespace, :uniq, # XXX untested, only applies to the polymorphic relationship # :finder_sql, # :counter_sql, # :before_add, # :after_add, # :before_remove, # :after_remove :dummy ) # validate against the most frequent configuration mistakes verify_pluralization_of(association_id) raise PolymorphicError, ":from option must be an array" unless [:from].is_a? Array # if an association with this name is already defined, we recreate it with # the new and old :from-options combined if self.reflections[association_id] [:from] += self.reflections[association_id].[:from] [:from].uniq! end [:from].each { |plural| verify_pluralization_of(plural) } [:as] ||= self.name.demodulize.underscore.to_sym [:conflicts] = Array([:conflicts]) [:foreign_key] ||= "#{[:as]}_id" [:association_foreign_key] = [:polymorphic_key] ||= "#{association_id._singularize}_id" [:polymorphic_type_key] ||= "#{association_id._singularize}_type" if .has_key? :ignore_duplicates _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates" [:skip_duplicates] = [:ignore_duplicates] end [:skip_duplicates] = true unless .has_key? :skip_duplicates [:dependent] = :destroy unless .has_key? :dependent [:conditions] = sanitize_sql([:conditions]) # options[:finder_sql] ||= "(options[:polymorphic_key] [:through] ||= build_join_table_symbol(association_id, ([:as]._pluralize or self.table_name)) # set up namespaces if we have a namespace key # XXX needs test coverage if [:namespace] namespace = [:namespace].to_s.chomp("/") + "/" [:from].map! do |child| "#{namespace}#{child}".to_sym end [:through] = "#{namespace}#{[:through]}".to_sym end [:join_class_name] ||= [:through]._classify [:table_aliases] ||= build_table_aliases([[:through]] + [:from]) [:select] ||= build_select(association_id, [:table_aliases]) [:through] = "#{[:through]}_as_#{[:singular_reverse_association_id]}" if [:singular_reverse_association_id] [:through] = demodulate([:through]).to_sym [:extend] = spiked_create_extension_module(association_id, Array([:extend]) + Array(extension)) [:join_extend] = spiked_create_extension_module(association_id, Array([:join_extend]), "Join") [:parent_extend] = spiked_create_extension_module(association_id, Array([:parent_extend]), "Parent") # create the reflection object create_reflection(:has_many_polymorphs, association_id, , self).tap do |reflection| # set up the other related associations create_join_association(association_id, reflection) create_has_many_through_associations_for_parent_to_children(association_id, reflection) create_has_many_through_associations_for_children_to_parent(association_id, reflection) end end |
#has_many_polymorphs(association_id, options = {}, &extension) ⇒ Object
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 totrue
. :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
becomeszoos_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-Rubyuniq
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.
196 197 198 199 200 201 202 |
# File 'lib/has_many_polymorphs/class_methods.rb', line 196 def has_many_polymorphs(association_id, = {}, &extension) _logger_debug "associating #{self}.#{association_id}" reflection = create_has_many_polymorphs_reflection(association_id, , &extension) # puts "Created reflection #{reflection.inspect}" # configure_dependency_for_has_many(reflection) collection_reader_method(reflection, PolymorphicAssociation) end |