aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/association_collection.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/association_collection.rb')
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb242
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