diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2005-12-03 04:29:55 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2005-12-03 04:29:55 +0000 |
commit | 6abda696b5df14a9ab132c34311daaabe12030e6 (patch) | |
tree | eb0cfad7daa4d46848e6ea10e1abd90ed93a3368 /activerecord/lib/active_record/associations | |
parent | 57b7532b910f9258cad4111db79349d2d63be6d4 (diff) | |
download | rails-6abda696b5df14a9ab132c34311daaabe12030e6.tar.gz rails-6abda696b5df14a9ab132c34311daaabe12030e6.tar.bz2 rails-6abda696b5df14a9ab132c34311daaabe12030e6.zip |
Added preliminary support for join models [DHH] Added preliminary support for polymorphic associations [DHH] Refactored associations to use reflections to get DRYer, beware, major refactoring -- double check before deploying anything with this (all tests pass, but..)
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3213 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib/active_record/associations')
8 files changed, 244 insertions, 215 deletions
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 7ee567e0b4..a66248ff2e 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,6 +18,7 @@ module ActiveRecord def <<(*records) result = true load_target + @owner.transaction do flatten_deeper(records).each do |record| raise_on_type_mismatch(record) @@ -28,7 +29,7 @@ module ActiveRecord end end - result and self + result && self end alias_method :push, :<< @@ -60,11 +61,13 @@ module ActiveRecord # Removes all records from this association. Returns +self+ so method calls may be chained. def clear return self if length.zero? # forces load_target if hasn't happened already - if @options[:exclusively_dependent] + + if @reflection.options[:exclusively_dependent] destroy_all else delete_all end + self end @@ -124,14 +127,6 @@ module ActiveRecord end private - def raise_on_type_mismatch(record) - raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class) - end - - def target_obsolete? - false - end - # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems. def flatten_deeper(array) array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten @@ -155,8 +150,8 @@ module ActiveRecord end def callbacks_for(callback_name) - full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}" - @owner.class.read_inheritable_attribute(full_callback_name.to_sym) or [] + full_callback_name = "#{callback_name}_for_#{@reflection.name}" + @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 8a7d925d0d..75f9184aa2 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -5,15 +5,9 @@ module ActiveRecord alias_method :proxy_extend, :extend instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ } - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) - @owner = owner - @options = options - @association_name = association_name - @association_class = eval(association_class_name, nil, __FILE__, __LINE__) - @association_class_primary_key_name = association_class_primary_key_name - - proxy_extend(options[:extend]) if options[:extend] - + def initialize(owner, reflection) + @owner, @reflection = owner, reflection + proxy_extend(reflection.options[:extend]) if reflection.options[:extend] reset end @@ -28,6 +22,11 @@ module ActiveRecord other === @target end + def reset + @target = nil + @loaded = false + end + def reload reset load_target @@ -45,14 +44,14 @@ module ActiveRecord @target end - def target=(t) - @target = t - @loaded = true + def target=(target) + @target = target + loaded end protected def dependent? - @options[:dependent] || false + @reflection.options[:dependent] || false end def quoted_record_ids(records) @@ -68,7 +67,7 @@ module ActiveRecord end def sanitize_sql(sql) - @association_class.send(:sanitize_sql, sql) + @reflection.klass.send(:sanitize_sql, sql) end def extract_options_from_args!(args) @@ -84,13 +83,14 @@ module ActiveRecord def load_target if !@owner.new_record? || foreign_key_present begin - @target = find_target if not loaded? + @target = find_target if !loaded? rescue ActiveRecord::RecordNotFound reset end end - @loaded = true if @target - @target + + loaded if target + target end # Can be overwritten by associations that might have the foreign key available for an association without @@ -100,7 +100,9 @@ module ActiveRecord end def raise_on_type_mismatch(record) - raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class) + unless record.is_a?(@reflection.klass) + raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}" + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 39d128aef1..804a7ebf21 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,41 +1,27 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) - super - construct_sql - end - - def reset - @target = nil - @loaded = false - end - def create(attributes = {}) - record = @association_class.create(attributes) - replace(record, true) - record + replace(@reflection.klass.create(attributes)) end def build(attributes = {}) - record = @association_class.new(attributes) - replace(record, true) - record + replace(@reflection.klass.new(attributes)) end - def replace(obj, dont_save = false) - if obj.nil? - @target = @owner[@association_class_primary_key_name] = nil + def replace(record) + if record.nil? + @target = @owner[@reflection.primary_key_name] = nil else - raise_on_type_mismatch(obj) unless obj.nil? + raise_on_type_mismatch(record) - @target = (AssociationProxy === obj ? obj.target : obj) - @owner[@association_class_primary_key_name] = obj.id unless obj.new_record? + @target = (AssociationProxy === record ? record.target : record) + @owner[@reflection.primary_key_name] = record.id unless record.new_record? @updated = true end - @loaded = true - return (@target.nil? ? nil : self) + loaded + record end def updated? @@ -44,27 +30,15 @@ module ActiveRecord private def find_target - if @options[:conditions] - @association_class.find( - @owner[@association_class_primary_key_name], - :conditions => interpolate_sql(@options[:conditions]), - :include => @options[:include] - ) - else - @association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include]) - end + @reflection.klass.find( + @owner[@reflection.primary_key_name], + :conditions => @reflection.options[:conditions] ? interpolate_sql(@reflection.options[:conditions]) : nil, + :include => @reflection.options[:include] + ) end def foreign_key_present - !@owner[@association_class_primary_key_name].nil? - end - - def target_obsolete? - @owner[@association_class_primary_key_name] != @target.id - end - - def construct_sql - @finder_sql = "#{@association_class.table_name}.#{@association_class.primary_key} = #{@owner.id}" + !@owner[@reflection.primary_key_name].nil? end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index e2a7f1a58e..6c2b9b89fd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,69 +1,49 @@ module ActiveRecord module Associations - class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) - @owner = owner - @options = options - @association_name = association_name - @association_class_primary_key_name = association_class_primary_key_name - - proxy_extend(options[:extend]) if options[:extend] - - reset - end - - def create(attributes = {}) - raise ActiveRecord::ActiveRecordError, "Can't create an abstract polymorphic object" - end - - def build(attributes = {}) - raise ActiveRecord::ActiveRecordError, "Can't build an abstract polymorphic object" - end - - def replace(obj, dont_save = false) - if obj.nil? - @target = @owner[@association_class_primary_key_name] = @owner[@options[:foreign_type]] = nil + class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: + def replace(record) + if record.nil? + @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil else - @target = (AssociationProxy === obj ? obj.target : obj) + @target = (AssociationProxy === record ? record.target : record) - unless obj.new_record? - @owner[@association_class_primary_key_name] = obj.id - @owner[@options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, obj.class).to_s + unless record.new_record? + @owner[@reflection.primary_key_name] = record.id + @owner[@reflection.options[:foreign_type]] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, record.class).to_s end @updated = true end - @loaded = true + loaded + record + end - return (@target.nil? ? nil : self) + def updated? + @updated end - + private def find_target return nil if association_class.nil? - if @options[:conditions] + if @reflection.options[:conditions] association_class.find( - @owner[@association_class_primary_key_name], - :conditions => interpolate_sql(@options[:conditions]), - :include => @options[:include] + @owner[@reflection.primary_key_name], + :conditions => interpolate_sql(@reflection.options[:conditions]), + :include => @reflection.options[:include] ) else - association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include]) + association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include]) end end def foreign_key_present - !@owner[@association_class_primary_key_name].nil? + !@owner[@reflection.primary_key_name].nil? end - def target_obsolete? - @owner[@association_class_primary_key_name] != @target.id - end - def association_class - @owner[@options[:foreign_type]] ? @owner[@options[:foreign_type]].constantize : nil + @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil end end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 4bfa759666..417b0905f4 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -1,20 +1,14 @@ module ActiveRecord module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + def initialize(owner, reflection) super - - @association_foreign_key = options[:association_foreign_key] || association_class_name.foreign_key - @association_table_name = options[:table_name] || @association_class.table_name - @join_table = options[:join_table] - @order = options[:order] - construct_sql end def build(attributes = {}) load_target - record = @association_class.new(attributes) + record = @reflection.klass.new(attributes) @target << record record end @@ -27,7 +21,7 @@ module ActiveRecord options = Base.send(:extract_options_from_args!, args) # If using a custom finder_sql, scan the entire collection. - if @options[:finder_sql] + if @reflection.options[:finder_sql] expects_array = args.first.kind_of?(Array) ids = args.flatten.compact.uniq @@ -40,60 +34,64 @@ module ActiveRecord end else conditions = "#{@finder_sql}" + if sanitized_conditions = sanitize_sql(options[:conditions]) conditions << " AND (#{sanitized_conditions})" end + options[:conditions] = conditions options[:joins] = @join_sql options[:readonly] ||= false - if options[:order] && @options[:order] - options[:order] = "#{options[:order]}, #{@options[:order]}" - elsif @options[:order] - options[:order] = @options[:order] + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] end # Pass through args exactly as we received them. args << options - @association_class.find(*args) + @reflection.klass.find(*args) end end def push_with_attributes(record, join_attributes = {}) raise_on_type_mismatch(record) join_attributes.each { |key, value| record[key.to_s] = value } + callback(:before_add, record) insert_record(record) unless @owner.new_record? @target << record callback(:after_add, record) + self end alias :concat_with_attributes :push_with_attributes def size - @options[:uniq] ? count_records : super + @reflection.options[:uniq] ? count_records : super end protected def method_missing(method, *args, &block) - if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method)) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) super else - @association_class.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do - @association_class.send(method, *args, &block) + @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do + @reflection.klass.send(method, *args, &block) end end end def find_target - if @options[:finder_sql] - records = @association_class.find_by_sql(@finder_sql) + if @reflection.options[:finder_sql] + records = @reflection.klass.find_by_sql(@finder_sql) else - records = find(:all, :include => @options[:include]) + records = find(:all, :include => @reflection.options[:include]) end - @options[:uniq] ? uniq(records) : records + @reflection.options[:uniq] ? uniq(records) : records end def count_records @@ -105,16 +103,16 @@ module ActiveRecord return false unless record.save end - if @options[:insert_sql] - @owner.connection.execute(interpolate_sql(@options[:insert_sql], record)) + if @reflection.options[:insert_sql] + @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record)) else - columns = @owner.connection.columns(@join_table, "#{@join_table} Columns") + columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") attributes = columns.inject({}) do |attributes, column| case column.name - when @association_class_primary_key_name + when @reflection.primary_key_name attributes[column.name] = @owner.quoted_id - when @association_foreign_key + when @reflection.association_foreign_key attributes[column.name] = record.quoted_id else if record.attributes.has_key?(column.name) @@ -126,7 +124,7 @@ module ActiveRecord end sql = - "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " + + "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " + "VALUES (#{attributes.values.join(', ')})" @owner.connection.execute(sql) @@ -136,26 +134,26 @@ module ActiveRecord end def delete_records(records) - if sql = @options[:delete_sql] + if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) } else ids = quoted_record_ids(records) - sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})" + sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})" @owner.connection.execute(sql) end end def construct_sql - interpolate_sql_options!(@options, :finder_sql) + interpolate_sql_options!(@reflection.options, :finder_sql) - if @options[:finder_sql] - @finder_sql = @options[:finder_sql] + if @reflection.options[:finder_sql] + @finder_sql = @reflection.options[:finder_sql] else - @finder_sql = "#{@join_table}.#{@association_class_primary_key_name} = #{@owner.quoted_id} " - @finder_sql << " AND (#{interpolate_sql(@options[:conditions])})" if @options[:conditions] + @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} " + @finder_sql << " AND (#{interpolate_sql(@reflection.options[:conditions])})" if @reflection.options[:conditions] end - @join_sql = "JOIN #{@join_table} ON #{@association_class.table_name}.#{@association_class.primary_key} = #{@join_table}.#{@association_foreign_key}" + @join_sql = "JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}" end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 465e1f8c72..f4a08420b7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -1,10 +1,9 @@ module ActiveRecord module Associations class HasManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + def initialize(owner, reflection) super - @conditions = sanitize_sql(options[:conditions]) - + @conditions = sanitize_sql(reflection.options[:conditions]) construct_sql end @@ -13,8 +12,8 @@ module ActiveRecord attributes.collect { |attr| create(attr) } else load_target - record = @association_class.new(attributes) - record[@association_class_primary_key_name] = @owner.id unless @owner.new_record? + record = @reflection.klass.new(attributes) + record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? @target << record record end @@ -22,13 +21,13 @@ module ActiveRecord # DEPRECATED. def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil) - if @options[:finder_sql] - @association_class.find_by_sql(@finder_sql) + if @reflection.options[:finder_sql] + @reflection.klass.find_by_sql(@finder_sql) else conditions = @finder_sql conditions += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions - orderings ||= @options[:order] - @association_class.find_all(conditions, orderings, limit, joins) + orderings ||= @reflection.options[:order] + @reflection.klass.find_all(conditions, orderings, limit, joins) end end @@ -39,14 +38,14 @@ module ActiveRecord # Count the number of associated records. All arguments are optional. def count(runtime_conditions = nil) - if @options[:counter_sql] - @association_class.count_by_sql(@counter_sql) - elsif @options[:finder_sql] - @association_class.count_by_sql(@finder_sql) + if @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) + elsif @reflection.options[:finder_sql] + @reflection.klass.count_by_sql(@finder_sql) else sql = @finder_sql sql += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions - @association_class.count(sql) + @reflection.klass.count(sql) end end @@ -54,7 +53,7 @@ module ActiveRecord options = Base.send(:extract_options_from_args!, args) # If using a custom finder_sql, scan the entire collection. - if @options[:finder_sql] + if @reflection.options[:finder_sql] expects_array = args.first.kind_of?(Array) ids = args.flatten.compact.uniq @@ -72,49 +71,49 @@ module ActiveRecord end options[:conditions] = conditions - if options[:order] && @options[:order] - options[:order] = "#{options[:order]}, #{@options[:order]}" - elsif @options[:order] - options[:order] = @options[:order] + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] end # Pass through args exactly as we received them. args << options - @association_class.find(*args) + @reflection.klass.find(*args) end end protected def method_missing(method, *args, &block) - if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method)) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) super else - @association_class.with_scope( + @reflection.klass.with_scope( :find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }, :create => { - @association_class_primary_key_name => @owner.id + @reflection.primary_key_name => @owner.id } ) do - @association_class.send(method, *args, &block) + @reflection.klass.send(method, *args, &block) end end end def find_target - if @options[:finder_sql] - @association_class.find_by_sql(@finder_sql) + if @reflection.options[:finder_sql] + @reflection.klass.find_by_sql(@finder_sql) else - @association_class.find(:all, + @reflection.klass.find(:all, :conditions => @finder_sql, - :order => @options[:order], - :limit => @options[:limit], - :joins => @options[:joins], - :include => @options[:include], - :group => @options[:group] + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :joins => @reflection.options[:joins], + :include => @reflection.options[:include], + :group => @reflection.options[:group] ) end end @@ -122,10 +121,10 @@ module ActiveRecord def count_records count = if has_cached_counter? @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @options[:counter_sql] - @association_class.count_by_sql(@counter_sql) + elsif @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) else - @association_class.count(@counter_sql) + @reflection.klass.count(@counter_sql) end @target = [] and loaded if count == 0 @@ -138,22 +137,22 @@ module ActiveRecord end def cached_counter_attribute_name - "#{@association_name}_count" + "#{@reflection.name}_count" end def insert_record(record) - record[@association_class_primary_key_name] = @owner.id + record[@reflection.primary_key_name] = @owner.id record.save end def delete_records(records) - if @options[:dependent] + if @reflection.options[:dependent] records.each { |r| r.destroy } else ids = quoted_record_ids(records) - @association_class.update_all( - "#{@association_class_primary_key_name} = NULL", - "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})" + @reflection.klass.update_all( + "#{@reflection.primary_key_name} = NULL", + "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" ) end end @@ -164,25 +163,25 @@ module ActiveRecord def construct_sql case - when @options[:as] + when @reflection.options[:finder_sql] + @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) + + when @reflection.options[:as] @finder_sql = - "#{@association_class.table_name}.#{@options[:as]}_id = #{@owner.quoted_id} AND " + - "#{@association_class.table_name}.#{@options[:as]}_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, @owner.class).to_s}'" + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, @owner.class).to_s}'" @finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions - when @options[:finder_sql] - @finder_sql = interpolate_sql(@options[:finder_sql]) - else - @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}" + @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" @finder_sql << " AND (#{interpolate_sql(@conditions)})" if @conditions end - if @options[:counter_sql] - @counter_sql = interpolate_sql(@options[:counter_sql]) - elsif @options[:finder_sql] - @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM") - @counter_sql = interpolate_sql(@options[:counter_sql]) + if @reflection.options[:counter_sql] + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + elsif @reflection.options[:finder_sql] + @reflection.options[:counter_sql] = @reflection.options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM") + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) else @counter_sql = @finder_sql end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb new file mode 100644 index 0000000000..ca4496b32e --- /dev/null +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -0,0 +1,80 @@ +module ActiveRecord + module Associations + class HasManyThroughAssociation < AssociationProxy #:nodoc: + def find(*args) + options = Base.send(:extract_options_from_args!, args) + + conditions = "#{@finder_sql}" + if sanitized_conditions = sanitize_sql(options[:conditions]) + conditions << " AND (#{sanitized_conditions})" + end + options[:conditions] = conditions + + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] + end + + # Pass through args exactly as we received them. + args << options + @reflection.klass.find(*args) + end + + def reset + @target = [] + @loaded = false + end + + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + else + @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) } + end + end + + def find_target + @reflection.klass.find(:all, + :conditions => construct_conditions, + :from => construct_from, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :joins => @reflection.options[:joins], + :group => @reflection.options[:group] + ) + end + + def construct_conditions + through_reflection = @owner.class.reflections[@reflection.options[:through]] + + if through_reflection.options[:as] + conditions = + "#{@reflection.table_name}.#{@reflection.klass.primary_key} = #{through_reflection.table_name}.#{@reflection.klass.to_s.foreign_key} " + + "AND #{through_reflection.table_name}.#{through_reflection.options[:as]}_id = #{@owner.quoted_id} " + + "AND #{through_reflection.table_name}.#{through_reflection.options[:as]}_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, @owner.class).to_s}'" + else + conditions = + "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{through_reflection.table_name}.#{@reflection.klass.to_s.foreign_key} " + + "AND #{through_reflection.table_name}.#{@owner.to_s.foreign_key} = #{@owner.quoted_id}" + end + + conditions << " AND (#{interpolate_sql(sanitize_sql(@reflection.options[:conditions]))})" if @reflection.options[:conditions] + + return conditions + end + + def construct_from + "#{@reflection.table_name}, #{@owner.class.reflections[@reflection.options[:through]].table_name}" + end + + def construct_scope + { + :find => { :conditions => construct_conditions }, + :create => { @reflection.primary_key_name => @owner.id } + } + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 8f7857ebea..17483305fc 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,9 +1,8 @@ module ActiveRecord module Associations class HasOneAssociation < BelongsToAssociation #:nodoc: - def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + def initialize(owner, reflection) super - construct_sql end @@ -14,12 +13,12 @@ module ActiveRecord end def build(attributes = {}, replace_existing = true) - record = @association_class.new(attributes) + record = @reflection.klass.new(attributes) if replace_existing replace(record, true) else - record[@association_class_primary_key_name] = @owner.id unless @owner.new_record? + record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? self.target = record end @@ -28,12 +27,13 @@ module ActiveRecord def replace(obj, dont_save = false) load_target + unless @target.nil? if dependent? && !dont_save && @target != obj @target.destroy unless @target.new_record? @owner.clear_association_cache else - @target[@association_class_primary_key_name] = nil + @target[@reflection.primary_key_name] = nil @target.save unless @owner.new_record? end end @@ -43,11 +43,12 @@ module ActiveRecord else raise_on_type_mismatch(obj) - obj[@association_class_primary_key_name] = @owner.id unless @owner.new_record? + obj[@reflection.primary_key_name] = @owner.id unless @owner.new_record? @target = (AssociationProxy === obj ? obj.target : obj) end @loaded = true + unless @owner.new_record? or obj.nil? or dont_save return (obj.save ? self : false) else @@ -57,16 +58,16 @@ module ActiveRecord private def find_target - @association_class.find(:first, :conditions => @finder_sql, :order => @options[:order], :include => @options[:include]) - end - - def target_obsolete? - false + @reflection.klass.find(:first, + :conditions => @finder_sql, + :order => @reflection.options[:order], + :include => @reflection.options[:include] + ) end def construct_sql - @finder_sql = "#{@association_class.table_name}.#{@association_class_primary_key_name} = #{@owner.quoted_id}" - @finder_sql << " AND (#{sanitize_sql(@options[:conditions])})" if @options[:conditions] + @finder_sql = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql << " AND (#{sanitize_sql(@reflection.options[:conditions])})" if @reflection.options[:conditions] @finder_sql end end |