1 module ActiveRecord #:nodoc:
2 module Associations #:nodoc:
4 class PolymorphicError < ActiveRecordError #:nodoc:
7 class PolymorphicMethodNotSupportedError < ActiveRecordError #:nodoc:
10 # The association class for a <tt>has_many_polymorphs</tt> association.
11 class PolymorphicAssociation < HasManyThroughAssociation
13 # Push a record onto the association. Triggers a database load for a uniqueness check only if <tt>:skip_duplicates</tt> is <tt>true</tt>. Return value is undefined.
15 return if records.empty?
17 if @reflection.options[:skip_duplicates]
18 _logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue"
22 @reflection.klass.transaction do
23 flatten_deeper(records).each do |record|
24 if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record?
25 raise PolymorphicError, "You can't associate unsaved records."
27 next if @reflection.options[:skip_duplicates] and @target.include? record
28 @owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record))
29 @target << record if loaded?
39 # Runs a <tt>find</tt> against the association contents, returning the matched records. All regular <tt>find</tt> options except <tt>:include</tt> are supported.
41 opts = args._extract_options!
43 super(*(args + [opts]))
47 _logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet."
51 # Deletes a record from the association. Return value is undefined.
53 records = flatten_deeper(records)
54 records.reject! {|record| @target.delete(record) if record.new_record?}
55 return if records.empty?
57 @reflection.klass.transaction do
58 records.each do |record|
59 joins = @reflection.through_reflection.name
60 @owner.send(joins).delete(@owner.send(joins).select do |join|
61 join.send(@reflection.options[:polymorphic_key]) == record.id and
62 join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}"
64 @target.delete(record)
69 # Clears all records from the association. Returns an empty array.
70 def clear(klass = nil)
72 return if @target.empty?
75 delete(@target.select {|r| r.is_a? klass })
77 @owner.send(@reflection.through_reflection.name).clear
88 def construct_quoted_owner_attributes(*args) #:nodoc:
89 # no access to returning() here? why not?
90 type_key = @reflection.options[:foreign_type_key]
91 {@reflection.primary_key_name => @owner.id,
92 type_key=> (@owner.class.base_class.name if type_key)}
95 def construct_from #:nodoc:
96 # build the FROM part of the query, in this case, the polymorphic join table
97 @reflection.klass.table_name
100 def construct_owner #:nodoc:
101 # the table name for the owner object's class
102 @owner.class.table_name
105 def construct_owner_key #:nodoc:
106 # the primary key field for the owner object
107 @owner.class.primary_key
110 def construct_select(custom_select = nil) #:nodoc:
111 # build the select query
112 selected = custom_select || @reflection.options[:select]
115 def construct_joins(custom_joins = nil) #:nodoc:
116 # build the string of default joins
117 "JOIN #{construct_owner} polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " +
118 @reflection.options[:from].map do |plural|
119 klass = plural._as_class
120 "LEFT JOIN #{klass.table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}"
121 end.uniq.join(" ") + " #{custom_joins}"
124 def construct_conditions #:nodoc:
125 # build the fully realized condition string
126 conditions = construct_quoted_owner_attributes.map do |field, value|
127 "#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value
129 conditions << custom_conditions if custom_conditions
130 "(" + conditions.compact.join(') AND (') + ")"
133 def custom_conditions #:nodoc:
134 # custom conditions... not as messy as has_many :through because our joins are a little smarter
135 if @reflection.options[:conditions]
136 "(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")"
140 alias :construct_owner_attributes :construct_quoted_owner_attributes
141 alias :conditions :custom_conditions # XXX possibly not necessary
142 alias :sql_conditions :custom_conditions # XXX ditto
144 # construct attributes for join for a particular record
145 def construct_join_attributes(record) #:nodoc:
146 {@reflection.options[:polymorphic_key] => record.id,
147 @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",
148 @reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ?
149 {@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships
152 def build(attrs = nil) #:nodoc:
153 raise PolymorphicMethodNotSupportedError, "You can't associate new records."