module ActiveRecord #:nodoc: module Associations #:nodoc: class PolymorphicError < ActiveRecordError #:nodoc: end class PolymorphicMethodNotSupportedError < ActiveRecordError #:nodoc: end # The association class for a has_many_polymorphs association. class PolymorphicAssociation < HasManyThroughAssociation # Push a record onto the association. Triggers a database load for a uniqueness check only if :skip_duplicates is true. Return value is undefined. def <<(*records) return if records.empty? if @reflection.options[:skip_duplicates] _logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue" load_target end @reflection.klass.transaction do flatten_deeper(records).each do |record| if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record? raise PolymorphicError, "You can't associate unsaved records." end next if @reflection.options[:skip_duplicates] and @target.include? record @owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record)) @target << record if loaded? end end self end alias :push :<< alias :concat :<< # Runs a find against the association contents, returning the matched records. All regular find options except :include are supported. def find(*args) opts = args._extract_options! opts.delete :include super(*(args + [opts])) end def construct_scope _logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet." super end # Deletes a record from the association. Return value is undefined. def delete(*records) records = flatten_deeper(records) records.reject! {|record| @target.delete(record) if record.new_record?} return if records.empty? @reflection.klass.transaction do records.each do |record| joins = @reflection.through_reflection.name @owner.send(joins).delete(@owner.send(joins).select do |join| join.send(@reflection.options[:polymorphic_key]) == record.id and join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}" end) @target.delete(record) end end end # Clears all records from the association. Returns an empty array. def clear(klass = nil) load_target return if @target.empty? if klass delete(@target.select {|r| r.is_a? klass }) else @owner.send(@reflection.through_reflection.name).clear @target.clear end [] end protected # undef :sum # undef :create! def construct_quoted_owner_attributes(*args) #:nodoc: # no access to returning() here? why not? type_key = @reflection.options[:foreign_type_key] {@reflection.primary_key_name => @owner.id, type_key=> (@owner.class.base_class.name if type_key)} end def construct_from #:nodoc: # build the FROM part of the query, in this case, the polymorphic join table @reflection.klass.table_name end def construct_owner #:nodoc: # the table name for the owner object's class @owner.class.table_name end def construct_owner_key #:nodoc: # the primary key field for the owner object @owner.class.primary_key end def construct_select(custom_select = nil) #:nodoc: # build the select query selected = custom_select || @reflection.options[:select] end def construct_joins(custom_joins = nil) #:nodoc: # build the string of default joins "JOIN #{construct_owner} polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " + @reflection.options[:from].map do |plural| klass = plural._as_class "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)}" end.uniq.join(" ") + " #{custom_joins}" end def construct_conditions #:nodoc: # build the fully realized condition string conditions = construct_quoted_owner_attributes.map do |field, value| "#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value end conditions << custom_conditions if custom_conditions "(" + conditions.compact.join(') AND (') + ")" end def custom_conditions #:nodoc: # custom conditions... not as messy as has_many :through because our joins are a little smarter if @reflection.options[:conditions] "(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")" end end alias :construct_owner_attributes :construct_quoted_owner_attributes alias :conditions :custom_conditions # XXX possibly not necessary alias :sql_conditions :custom_conditions # XXX ditto # construct attributes for join for a particular record def construct_join_attributes(record) #:nodoc: {@reflection.options[:polymorphic_key] => record.id, @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}", @reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ? {@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships end def build(attrs = nil) #:nodoc: raise PolymorphicMethodNotSupportedError, "You can't associate new records." end end end end