diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
3 files changed, 92 insertions, 15 deletions
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 5092ccc1dc..168443e092 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 @@ -214,9 +227,16 @@ 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 @@ -228,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 diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 5427681f3c..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 + # 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/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 1838021d40..dda22668c6 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -1,5 +1,9 @@ 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: protected def owner_quoted_id @@ -10,6 +14,16 @@ module ActiveRecord end end + # Returns the number of records in this collection. + # + # If the association has a counter cache it gets that value. Otherwise + # a count via SQL is performed, bounded to <tt>:limit</tt> if there's one. + # 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) |