diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/association_collection.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/association_collection.rb | 242 |
1 files changed, 117 insertions, 125 deletions
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b75e02c66b..2811f53424 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -22,8 +22,7 @@ module ActiveRecord def select(select = nil) if block_given? - load_target - @target.select.each { |e| yield e } + load_target.select.each { |e| yield e } else scoped.select(select) end @@ -33,45 +32,27 @@ module ActiveRecord if @reflection.options[:finder_sql] find_by_scan(*args) else - find_by_sql(*args) + scoped.find(*args) end end - # Fetches the first one using SQL if possible. def first(*args) - if fetch_first_or_last_using_find?(args) - find(:first, *args) - else - load_target unless loaded? - args.shift if args.first.kind_of?(Hash) && args.first.empty? - @target.first(*args) - end + first_or_last(:first, *args) end - # Fetches the last one using SQL if possible. def last(*args) - if fetch_first_or_last_using_find?(args) - find(:last, *args) - else - load_target unless loaded? - @target.last(*args) - end + first_or_last(:last, *args) end def to_ary - load_target - if @target.is_a?(Array) - @target - else - Array.wrap(@target) - end + load_target.dup end alias_method :to_a, :to_ary def reset - reset_target! - reset_scopes_cache! - @loaded = false + @_scopes_cache = {} + @loaded = false + @target = [] end def build(attributes = {}, &block) @@ -125,18 +106,35 @@ module ActiveRecord # # See delete for more info. def delete_all - load_target - delete(@target) - reset_target! - reset_scopes_cache! + delete(load_target).tap do + reset + loaded! + end + end + + # Identical to delete_all, except that the return value is the association (for chaining) + # rather than the records which have been removed. + def clear + delete_all + self + end + + # Destroy all the records from this association. + # + # See destroy for more info. + def destroy_all + destroy(load_target).tap do + reset + loaded! + end end # Calculate sum using SQL, not Enumerable def sum(*args) if block_given? - calculate(:sum, *args) { |*block_args| yield(*block_args) } + scoped.sum(*args) { |*block_args| yield(*block_args) } else - calculate(:sum, *args) + scoped.sum(*args) end end @@ -155,7 +153,7 @@ module ActiveRecord else if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name + column_name ||= @reflection.klass.primary_key options.merge!(:distinct => true) end @@ -200,30 +198,6 @@ module ActiveRecord load_target end - # Removes all records from this association. Returns +self+ so method calls may be chained. - def clear - unless length.zero? # forces load_target if it hasn't happened already - if @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all - end - end - - self - end - - # Destroy all the records from this association. - # - # See destroy for more info. - def destroy_all - load_target - destroy(@target).tap do - reset_target! - reset_scopes_cache! - end - end - def create(attrs = {}) if attrs.is_a?(Array) attrs.collect { |attr| create(attr) } @@ -283,7 +257,7 @@ module ActiveRecord def any? if block_given? - method_missing(:any?) { |*block_args| yield(*block_args) } + load_target.any? { |*block_args| yield(*block_args) } else !empty? end @@ -292,7 +266,7 @@ module ActiveRecord # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. def many? if block_given? - method_missing(:many?) { |*block_args| yield(*block_args) } + load_target.many? { |*block_args| yield(*block_args) } else size > 1 end @@ -309,13 +283,13 @@ module ActiveRecord # This will perform a diff and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } - - load_target + original_target = load_target.dup transaction do delete(@target - other_array) unless concat(other_array - @target) + @target = original_target raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " "new records could not be saved." end @@ -323,16 +297,40 @@ module ActiveRecord end def include?(record) - return false unless record.is_a?(@reflection.klass) - return include_in_memory?(record) if record.new_record? - load_target if @reflection.options[:finder_sql] && !loaded? - loaded? ? @target.include?(record) : exists?(record) + if record.is_a?(@reflection.klass) + if record.new_record? + include_in_memory?(record) + else + load_target if @reflection.options[:finder_sql] + loaded? ? @target.include?(record) : scoped.exists?(record) + end + else + false + end end def respond_to?(method, include_private = false) super || @reflection.klass.respond_to?(method, include_private) end + def method_missing(method, *args, &block) + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + end + + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes[method] + @_scopes_cache ||= {} + @_scopes_cache[method] ||= {} + @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + else + scoped.readonly(nil).send(method, *args, &block) + end + end + protected def association_scope @@ -340,16 +338,8 @@ module ActiveRecord super.apply_finder_options(options) end - def select_value - super || uniq_select_value - end - - def uniq_select_value - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - def load_target - if (!@owner.new_record? || foreign_key_present?) && !loaded? + if find_target? targets = [] begin @@ -361,26 +351,32 @@ module ActiveRecord @target = merge_target_lists(targets, @target) end - loaded + loaded! target end - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) - end - - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - super - elsif @reflection.klass.scopes[method] - @_scopes_cache ||= {} - @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + def add_record_to_target_with_callbacks(record) + callback(:before_add, record) + yield(record) if block_given? + @target ||= [] unless loaded? + if @reflection.options[:uniq] && index = @target.index(record) + @target[index] = record else - scoped.readonly(nil).send(method, *args, &block) + @target << record end + callback(:after_add, record) + set_inverse_instance(record) + record + end + + private + + def select_value + super || uniq_select_value + end + + def uniq_select_value + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def custom_counter_sql @@ -398,14 +394,6 @@ module ActiveRecord interpolate_sql(@reflection.options[:finder_sql]) end - def reset_target! - @target = [] - end - - def reset_scopes_cache! - @_scopes_cache = {} - end - def find_target records = if @reflection.options[:finder_sql] @@ -419,21 +407,6 @@ module ActiveRecord records end - def add_record_to_target_with_callbacks(record) - callback(:before_add, record) - yield(record) if block_given? - @target ||= [] unless loaded? - if @reflection.options[:uniq] && index = @target.index(record) - @target[index] = record - else - @target << record - end - callback(:after_add, record) - set_inverse_instance(record) - record - end - - private def merge_target_lists(loaded, existing) return loaded if existing.empty? return existing if loaded.empty? @@ -466,17 +439,14 @@ module ActiveRecord force ? record.save! : record.save(:validate => validate) end - def create_record(attrs, &block) + def create_record(attributes, &block) ensure_owner_is_persisted! - - transaction do - scoped.scoping { build_record(attrs, &block) } - end + transaction { build_record(attributes, &block) } end - def build_record(attrs, &block) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.build_association(attrs) + def build_record(attributes, &block) + attributes = scoped.scope_for_create.merge(attributes) + record = @reflection.build_association(attributes) add_record_to_target_with_callbacks(record, &block) end @@ -516,9 +486,27 @@ module ActiveRecord end end + # Should we deal with assoc.first or assoc.last by issuing an independent query to + # the database, or by getting the target, and then taking the first/last item from that? + # + # If the args is just a non-empty options hash, go to the database. + # + # Otherwise, go to the database only if none of the following are true: + # * target already loaded + # * owner is new record + # * custom :finder_sql exists + # * target contains new or changed record(s) + # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) - (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) + if args.first.is_a?(Hash) + true + else + !(loaded? || + @owner.new_record? || + @reflection.options[:finder_sql] || + @target.any? { |record| record.new_record? || record.changed? } || + args.first.kind_of?(Integer)) + end end def include_in_memory?(record) @@ -546,8 +534,12 @@ module ActiveRecord end end - def find_by_sql(*args) - scoped.find(*args) + # Fetches the first/last using SQL if possible, otherwise from the target array. + def first_or_last(type, *args) + args.shift if args.first.is_a?(Hash) && args.first.empty? + + collection = fetch_first_or_last_using_find?(args) ? scoped : load_target + collection.send(type, *args) end end end |