aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations')
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb79
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb61
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb4
8 files changed, 165 insertions, 62 deletions
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 9061037b39..afb817f8ae 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -2,6 +2,19 @@ require 'set'
module ActiveRecord
module Associations
+ # AssociationCollection is an abstract class that provides common stuff to
+ # ease the implementation of association proxies that represent
+ # collections. See the class hierarchy in AssociationProxy.
+ #
+ # You need to be careful with assumptions regarding the target: The proxy
+ # does not fetch records from the database until it needs them, but new
+ # ones created with +build+ are added to the target. So, the target may be
+ # non-empty and still lack children waiting to be read from the database.
+ # If you look directly to the database you cannot assume that's the entire
+ # collection because new records may have beed added to the target, etc.
+ #
+ # If you need to work on all current children, new and existing records,
+ # +load_target+ and the +loaded+ flag are your friends.
class AssociationCollection < AssociationProxy #:nodoc:
def initialize(owner, reflection)
super
@@ -97,8 +110,6 @@ module ActiveRecord
@owner.transaction do
flatten_deeper(records).each do |record|
- record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash)
-
raise_on_type_mismatch(record)
add_record_to_target_with_callbacks(record) do |r|
result &&= insert_record(record) unless @owner.new_record?
@@ -128,6 +139,35 @@ module ActiveRecord
end
end
+ # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
+ # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
+ # descendant's +construct_sql+ method will have set :counter_sql automatically.
+ # Otherwise, construct options and pass them with scope to the target class's +count+.
+ def count(*args)
+ if @reflection.options[:counter_sql]
+ @reflection.klass.count_by_sql(@counter_sql)
+ else
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
+ 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}" if column_name == :all
+ options.merge!(:distinct => true)
+ end
+
+ value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
+
+ limit = @reflection.options[:limit]
+ offset = @reflection.options[:offset]
+
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
+ end
+ end
+ end
+
+
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
@@ -185,12 +225,21 @@ module ActiveRecord
end
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
- # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
- # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
+ # Returns the size of the collection by executing a SELECT COUNT(*)
+ # query if the collection hasn't been loaded, and calling
+ # <tt>collection.size</tt> if it has.
+ #
+ # If the collection has been already loaded +size+ and +length+ are
+ # equivalent. If not and you are going to need the records anyway
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
+ #
+ # This method is abstract in the sense that it relies on
+ # +count_records+, which is a method descendants have to provide.
def size
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size
+ elsif !loaded? && @reflection.options[:group]
+ load_target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = @target.select { |r| r.new_record? }
unsaved_records.size + count_records
@@ -199,12 +248,18 @@ module ActiveRecord
end
end
- # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
- # whether the collection is empty, use collection.length.zero? instead of collection.empty?
+ # Returns the size of the collection calling +size+ on the target.
+ #
+ # If the collection has been already loaded +length+ and +size+ are
+ # equivalent. If not and you are going to need the records anyway this
+ # method will take one less query. Otherwise +size+ is more efficient.
def length
load_target.size
end
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
+ # not been already loaded and you are going to fetch the records anyway
+ # it is better to check <tt>collection.length.zero?</tt>.
def empty?
size.zero?
end
@@ -231,10 +286,6 @@ module ActiveRecord
# Replace this collection with +other_array+
# This will perform a diff and delete/add only records that have changed.
def replace(other_array)
- other_array.map! do |val|
- val.is_a?(Hash) ? @reflection.klass.new(val) : val
- end if @reflection.options[:accessible]
-
other_array.each { |val| raise_on_type_mismatch(val) }
load_target
@@ -322,7 +373,9 @@ module ActiveRecord
def create_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
ensure_owner_is_not_new
- record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
+ @reflection.build_association(attrs)
+ end
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
@@ -332,7 +385,7 @@ module ActiveRecord
def build_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
- record = @reflection.klass.new(attrs)
+ record = @reflection.build_association(attrs)
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 99b8748a48..acdcd14ec8 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -39,7 +39,7 @@ module ActiveRecord
# though the object behind <tt>blog.posts</tt> is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
- # The <tt>@target</tt> object is not loaded until needed. For example,
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
#
# blog.posts.count
#
@@ -57,76 +57,100 @@ module ActiveRecord
reset
end
+ # Returns the owner of the proxy.
def proxy_owner
@owner
end
+ # Returns the reflection object that represents the association handled
+ # by the proxy.
def proxy_reflection
@reflection
end
+ # Returns the \target of the proxy, same as +target+.
def proxy_target
@target
end
- def respond_to?(symbol, include_priv = false)
- proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
+ # Does the proxy or its \target respond to +symbol+?
+ def respond_to?(*args)
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
- # Explicitly proxy === because the instance method removal above
- # doesn't catch it.
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
+ # removal above doesn't catch it. Loads the \target if needed.
def ===(other)
load_target
other === @target
end
+ # Returns the name of the table of the related class:
+ #
+ # post.comments.aliased_table_name # => "comments"
+ #
def aliased_table_name
@reflection.klass.table_name
end
+ # Returns the SQL string that corresponds to the <tt>:conditions</tt>
+ # option of the macro, if given, or +nil+ otherwise.
def conditions
@conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
end
alias :sql_conditions :conditions
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
@target = nil
end
+ # Reloads the \target and returns +self+ on success.
def reload
reset
load_target
self unless @target.nil?
end
+ # Has the \target been already \loaded?
def loaded?
@loaded
end
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded
@loaded = true
end
+ # Returns the target of this proxy, same as +proxy_target+.
def target
@target
end
+ # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
def target=(target)
@target = target
loaded
end
+ # Forwards the call to the target. Loads the \target if needed.
def inspect
load_target
@target.inspect
end
protected
+ # Does the association have a <tt>:dependent</tt> option?
def dependent?
@reflection.options[:dependent]
end
+ # Returns a string with the IDs of +records+ joined with a comma, quoted
+ # if needed. The result is ready to be inserted into a SQL IN clause.
+ #
+ # quoted_record_ids(records) # => "23,56,58,67"
+ #
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
@@ -135,10 +159,13 @@ module ActiveRecord
@owner.send(:interpolate_sql, sql, record)
end
+ # Forwards the call to the reflection class.
def sanitize_sql(sql)
@reflection.klass.send(:sanitize_sql, sql)
end
+ # Assigns the ID of the owner to the corresponding foreign key in +record+.
+ # If the association is polymorphic the type of the owner is also set.
def set_belongs_to_association_for(record)
if @reflection.options[:as]
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
@@ -148,6 +175,7 @@ module ActiveRecord
end
end
+ # Merges into +options+ the ones coming from the reflection.
def merge_options_from_reflection!(options)
options.reverse_merge!(
:group => @reflection.options[:group],
@@ -160,11 +188,13 @@ module ActiveRecord
)
end
+ # Forwards +with_scope+ to the reflection.
def with_scope(*args, &block)
@reflection.klass.send :with_scope, *args, &block
end
private
+ # Forwards any missing method call to the \target.
def method_missing(method, *args)
if load_target
if block_given?
@@ -175,16 +205,16 @@ module ActiveRecord
end
end
- # Loads the target if needed and returns it.
+ # Loads the \target if needed and returns it.
#
# This method is abstract in the sense that it relies on +find_target+,
# which is expected to be provided by descendants.
#
- # If the target is already loaded it is just returned. Thus, you can call
- # +load_target+ unconditionally to get the target.
+ # If the \target is already \loaded it is just returned. Thus, you can call
+ # +load_target+ unconditionally to get the \target.
#
# ActiveRecord::RecordNotFound is rescued within the method, and it is
- # not reraised. The proxy is reset and +nil+ is the return value.
+ # not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
return nil unless defined?(@loaded)
@@ -198,12 +228,17 @@ module ActiveRecord
reset
end
- # Can be overwritten by associations that might have the foreign key available for an association without
- # having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
+ # Can be overwritten by associations that might have the foreign key
+ # available for an association without having the object itself (and
+ # still being a new record). Currently, only +belongs_to+ presents
+ # this scenario (both vanilla and polymorphic).
def foreign_key_present
false
end
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
+ # the kind of the class of the associated objects. Meant to be used as
+ # a sanity check when you are about to assign an associated record.
def raise_on_type_mismatch(record)
unless record.is_a?(@reflection.klass)
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
@@ -211,11 +246,13 @@ module ActiveRecord
end
end
- # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
+ # 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.is_a?(Hash)) ? element.flatten : element }.flatten
end
+ # Returns the ID of the owner, quoted if needed.
def owner_quoted_id
@owner.quoted_id
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 7c28cbdd07..f05c6be075 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -2,11 +2,11 @@ module ActiveRecord
module Associations
class BelongsToAssociation < AssociationProxy #:nodoc:
def create(attributes = {})
- replace(@reflection.klass.create(attributes))
+ replace(@reflection.create_association(attributes))
end
def build(attributes = {})
- replace(@reflection.klass.new(attributes))
+ replace(@reflection.build_association(attributes))
end
def replace(record)
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 e7e433b6b6..3d689098b5 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
@@ -78,6 +78,16 @@ module ActiveRecord
end
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
+
+ if @reflection.options[:counter_sql]
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+ elsif @reflection.options[:finder_sql]
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+ else
+ @counter_sql = @finder_sql
+ end
end
def construct_scope
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index ce62127505..3b2f306637 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -1,23 +1,10 @@
module ActiveRecord
module Associations
+ # This is the proxy that handles a has many association.
+ #
+ # If the association has a <tt>:through</tt> option further specialization
+ # is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc:
- # Count the number of associated records. All arguments are optional.
- def count(*args)
- 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
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
- options[:conditions] = options[:conditions].blank? ?
- @finder_sql :
- @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
- options[:include] ||= @reflection.options[:include]
-
- @reflection.klass.count(column_name, options)
- end
- end
-
protected
def owner_quoted_id
if @reflection.options[:primary_key]
@@ -27,6 +14,19 @@ module ActiveRecord
end
end
+ # Returns the number of records in this collection.
+ #
+ # If the association has a counter cache it gets that value. Otherwise
+ # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
+ # there's one. Some configuration options like :group make it impossible
+ # to do a SQL count, in those cases the array count will be used.
+ #
+ # That does not depend on whether the collection has already been loaded
+ # or not. The +size+ method is the one that takes the loaded flag into
+ # account and delegates to +count_records+ if needed.
+ #
+ # If the collection is empty the target is set to an empty array and
+ # the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
@owner.send(:read_attribute, cached_counter_attribute_name)
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 24b02efc35..ebd2bf768c 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -10,14 +10,14 @@ module ActiveRecord
def create!(attrs = nil)
@reflection.klass.transaction do
- self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!)
+ self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
object
end
end
def create(attrs = nil)
@reflection.klass.transaction do
- self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create)
+ self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association)
object
end
end
@@ -31,16 +31,6 @@ module ActiveRecord
return count
end
- def count(*args)
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
- if @reflection.options[:uniq]
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement.
- column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
- options.merge!(:distinct => true)
- end
- @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
- end
-
protected
def construct_find_options!(options)
options[:select] = construct_select(options[:select])
@@ -57,8 +47,9 @@ module ActiveRecord
return false unless record.save
end
end
- klass = @reflection.through_reflection.klass
- @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! }
+ through_reflection = @reflection.through_reflection
+ klass = through_reflection.klass
+ @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
end
# TODO - add dependent option support
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 18733255d2..c92ef5c2c9 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -7,15 +7,21 @@ module ActiveRecord
end
def create(attrs = {}, replace_existing = true)
- new_record(replace_existing) { |klass| klass.create(attrs) }
+ new_record(replace_existing) do |reflection|
+ reflection.create_association(attrs)
+ end
end
def create!(attrs = {}, replace_existing = true)
- new_record(replace_existing) { |klass| klass.create!(attrs) }
+ new_record(replace_existing) do |reflection|
+ reflection.create_association!(attrs)
+ end
end
def build(attrs = {}, replace_existing = true)
- new_record(replace_existing) { |klass| klass.new(attrs) }
+ new_record(replace_existing) do |reflection|
+ reflection.build_association(attrs)
+ end
end
def replace(obj, dont_save = false)
@@ -91,7 +97,9 @@ module ActiveRecord
# instance. Otherwise, if the target has not previously been loaded
# elsewhere, the instance we create will get orphaned.
load_target if replace_existing
- record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
+ yield @reflection
+ end
if replace_existing
replace(record, true)
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index c846956e1f..b78bd5d931 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -22,6 +22,10 @@ module ActiveRecord
def find_target
super.first
+ end
+
+ def reset_target!
+ @target = nil
end
end
end