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

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 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.



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 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
      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

      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(/_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

#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.

Raises:



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, options, &extension) #:nodoc:
  options.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 options[: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]
    options[:from] += self.reflections[association_id].options[:from]
    options[:from].uniq!
  end
  options[:from].each { |plural| verify_pluralization_of(plural) }

  options[:as] ||= self.name.demodulize.underscore.to_sym
  options[:conflicts] = Array(options[:conflicts])
  options[:foreign_key] ||= "#{options[:as]}_id"

  options[:association_foreign_key] =
    options[:polymorphic_key] ||= "#{association_id._singularize}_id"
  options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"

  if options.has_key? :ignore_duplicates
    _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
    options[:skip_duplicates] = options[:ignore_duplicates]
  end
  options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
  options[:dependent] = :destroy unless options.has_key? :dependent
  options[:conditions] = sanitize_sql(options[:conditions])

  # options[:finder_sql] ||= "(options[:polymorphic_key]

  options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))

  # set up namespaces if we have a namespace key
  # XXX needs test coverage
  if options[:namespace]
    namespace = options[:namespace].to_s.chomp("/") + "/"
    options[:from].map! do |child|
      "#{namespace}#{child}".to_sym
    end
    options[:through] = "#{namespace}#{options[:through]}".to_sym
  end

  options[:join_class_name] ||= options[:through]._classify
  options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
  options[:select] ||= build_select(association_id, options[:table_aliases])

  options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
  options[:through] = demodulate(options[:through]).to_sym

  options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
  options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
  options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")

  # create the reflection object
  create_reflection(:has_many_polymorphs, association_id, options, 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 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.



196
197
198
199
200
201
202
# File 'lib/has_many_polymorphs/class_methods.rb', line 196

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