diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
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 |