123456789_123456789_123456789_123456789_123456789_

Class: ActiveRecord::Associations::CollectionAssociation

Do not use. This class is for internal use only.
Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Subclasses:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Association
Instance Chain:
self, Association
Inherits: ActiveRecord::Associations::Association
Defined in: activerecord/lib/active_record/associations/collection_association.rb

Overview

Active Record Association Collection

CollectionAssociation is an abstract class that provides common stuff to ease the implementation of association proxies that represent collections. See the class hierarchy in Association.

CollectionAssociation:
  HasManyAssociation => has_many
    HasManyThroughAssociation + ThroughAssociation => has_many :through

The CollectionAssociation class provides common methods to the collections defined by has_and_belongs_to_many, has_many or has_many with the :through association option.

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 been 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 Method Summary

Association - Inherited

Instance Attribute Summary

Association - Inherited

#collection?

Whether the association represent a single record or a collection of records.

#disable_joins,
#loaded?

Has the target been already loaded?

#options, #owner, #reflection,
#stale_target?

The target is stale if the target no longer points to the record(s) that the relevant foreign_key(s) refers to.

#target,
#target=

Sets the target of this association to \target, and the loaded flag to true.

#find_target?,
#foreign_key_present?

Returns true if there is a foreign key present on the owner which references the target.

#violates_strict_loading?

Instance Method Summary

Association - Inherited

#async_load_target, #create, #create!, #extensions, #initialize_attributes, #inversed_from, #inversed_from_queries,
#klass

Returns the class of the target.

#load_target

Loads the target if needed and returns it.

#loaded!

Asserts the target has been loaded setting the loaded flag to true.

#marshal_dump

We can’t dump @reflection and @through_reflection since it contains the scope proc.

#marshal_load,
#reload

Reloads the target and returns self on success.

#remove_inverse_instance

Remove the inverse association, if possible.

#reset

Resets the loaded flag to false and sets the target to nil.

#reset_negative_cache, #reset_scope, #scope,
#set_inverse_instance

Set the inverse association, if possible.

#set_inverse_instance_from_queries, #set_strict_loading,
#association_scope

The scope for this association.

#build_record, #enqueue_destroy_association,
#ensure_klass_exists!

Reader and writer methods call this so that consistent errors are presented when the association target class does not exist.

#find_target,
#foreign_key_for?

Returns true if record contains the foreign_key.

#inversable?, #inverse_association_for,
#inverse_reflection_for

Can be redefined by subclasses, notably polymorphic belongs_to The record parameter is necessary to support polymorphic inverses as we must check for the association in the specific class of the record.

#invertible_for?

Returns true if inverse association on the given record needs to be set.

#matches_foreign_key?,
#raise_on_type_mismatch!

Raises ::ActiveRecord::AssociationTypeMismatch unless record is of the kind of the class of the associated objects.

#scope_for_create,
#skip_statement_cache?

Returns true if statement cache should be skipped on the association reader.

#skip_strict_loading,
#stale_state

This should be implemented to return the values of the relevant key(s) on the owner, so that when stale_state is different from the value stored on the last find_target, the target is stale.

#target_scope

Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e.

Constructor Details

This class inherits a constructor from ActiveRecord::Associations::Association

Instance Attribute Details

#collection?Boolean (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 316

def collection?
  true
end

#empty?Boolean (readonly)

Returns true if the collection is empty.

If the collection has been loaded it is equivalent to collection.size.zero?. If the collection has not been loaded, it is equivalent to !collection.exists?. If the collection has not already been loaded and you are going to fetch the records anyway it is better to check collection.length.zero?.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 232

def empty?
  if loaded? || @association_ids || reflection.has_active_cached_counter?
    size.zero?
  else
    target.empty? && !scope.exists?
  end
end

#find_from_target?Boolean (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 308

def find_from_target?
  loaded? ||
    (owner.strict_loading? && owner.strict_loading_all?) ||
    reflection.strict_loading? ||
    owner.new_record? ||
    target.any? { |record| record.new_record? || record.changed? }
end

#nested_attributes_target (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 31

attr_accessor :nested_attributes_target

#null_scope?Boolean (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 304

def null_scope?
  owner.new_record? && !foreign_key_present?
end

#target=(record) (writeonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 285

def target=(record)
  return super unless reflection.klass.has_many_inversing

  case record
  when nil
    # It's not possible to remove the record from the inverse association.
  when Array
    super
  else
    replace_on_target(record, true, replace: true, inversing: true)
  end
end

Instance Method Details

#_create_record(attributes, raise = false, &block) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 354

def _create_record(attributes, raise = false, &block)
  unless owner.persisted?
    raise ActiveRecord::RecordNotSaved.new("You cannot call create unless the parent is saved", owner)
  end

  if attributes.is_a?(Array)
    attributes.collect { |attr| _create_record(attr, raise, &block) }
  else
    record = build_record(attributes, &block)
    transaction do
      result = nil
      add_to_target(record) do
        result = insert_record(record, true, raise) {
          @_was_loaded = loaded?
        }
      end
      raise ActiveRecord::Rollback unless result
    end
    record
  end
end

#add_to_target(record, skip_callbacks: false, replace: false, &block)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 281

def add_to_target(record, skip_callbacks: false, replace: false, &block)
  replace_on_target(record, skip_callbacks, replace: replace || association_scope.distinct_value, &block)
end

#build(attributes = nil, &block)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 117

def build(attributes = nil, &block)
  if attributes.is_a?(Array)
    attributes.collect { |attr| build(attr, &block) }
  else
    add_to_target(build_record(attributes, &block), replace: true)
  end
end

#callback(method, record) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 492

def callback(method, record)
  callbacks_for(method).each do |callback|
    callback.call(method, owner, record)
  end
end

#callbacks_for(callback_name) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 498

def callbacks_for(callback_name)
  full_callback_name = "#{callback_name}_for_#{reflection.name}"
  if owner.class.respond_to?(full_callback_name)
    owner.class.send(full_callback_name)
  else
    []
  end
end

#concat(*records)

Add records to this association. Since << flattens its argument list and inserts each record, push and concat behave identically.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 127

def concat(*records)
  records = records.flatten
  if owner.new_record?
    skip_strict_loading { load_target }
    concat_records(records)
  else
    transaction { concat_records(records) }
  end
end

#concat_records(records, raise = false) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 438

def concat_records(records, raise = false)
  result = true

  records.each do |record|
    raise_on_type_mismatch!(record)
    add_to_target(record) do
      unless owner.new_record?
        result &&= insert_record(record, true, raise) {
          @_was_loaded = loaded?
        }
      end
    end
  end

  raise ActiveRecord::Rollback unless result

  records
end

#delete(*records)

Removes records from this association calling before_remove and after_remove callbacks.

This method is abstract in the sense that #delete_records has to be provided by descendants. Note this method does not imply the records are actually removed from the database, that depends precisely on #delete_records. They are in any case removed from the collection.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 186

def delete(*records)
  delete_or_destroy(records, options[:dependent])
end

#delete_all(dependent = nil)

Removes all records from the association without calling callbacks on the associated records. It honors the :dependent option. However if the :dependent value is :destroy then in that case the :delete_all deletion strategy for the association is applied.

You can force a particular deletion strategy by passing a parameter.

Example:

@author.books.delete_all(:nullify) @author.books.delete_all(:delete_all)

See delete for more info.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 150

def delete_all(dependent = nil)
  if dependent && ![:nullify, :delete_all].include?(dependent)
    raise ArgumentError, "Valid values are :nullify or :delete_all"
  end

  dependent = if dependent
    dependent
  elsif options[:dependent] == :destroy
    :delete_all
  else
    options[:dependent]
  end

  delete_or_nullify_all_records(dependent).tap do
    reset
    loaded!
  end
end

#delete_or_destroy(records, method) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 385

def delete_or_destroy(records, method)
  return if records.empty?
  records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
  records = records.flatten
  records.each { |record| raise_on_type_mismatch!(record) }
  existing_records = records.reject(&:new_record?)

  if existing_records.empty?
    remove_records(existing_records, records, method)
  else
    transaction { remove_records(existing_records, records, method) }
  end
end

#delete_records(records, method) (private)

Delete the given records from the association, using one of the methods :destroy, :delete_all or :nullify (or nil, in which case a default is used).

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 414

def delete_records(records, method)
  raise NotImplementedError
end

#destroy(*records)

Deletes the records and removes them from this association calling before_remove, after_remove, before_destroy and after_destroy callbacks.

Note that this method removes records from the database ignoring the :dependent option.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 195

def destroy(*records)
  delete_or_destroy(records, :destroy)
end

#destroy_all

Destroy all the records from this association.

See destroy for more info.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 172

def destroy_all
  destroy(load_target).tap do
    reset
    loaded!
  end
end

#find(*args)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 94

def find(*args)
  if options[:inverse_of] && loaded?
    args_flatten = args.flatten
    model = scope.model

    if args_flatten.blank?
      error_message = "Couldn't find #{model.name} without an ID"
      raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
    end

    result = find_by_scan(*args)

    result_size = Array(result).size
    if !result || result_size != args_flatten.size
      scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
    else
      result
    end
  else
    scope.find(*args)
  end
end

#find_by_scan(*args) (private)

If the :inverse_of option has been specified, then #find scans the entire collection.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 521

def find_by_scan(*args)
  expects_array = args.first.kind_of?(Array)
  ids           = args.flatten.compact.map(&:to_s).uniq

  if ids.size == 1
    id = ids.first
    record = load_target.detect { |r| id == r.id.to_s }
    expects_array ? [ record ] : record
  else
    load_target.select { |r| ids.include?(r.id.to_s) }
  end
end

#ids_reader

Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 51

def ids_reader
  if loaded?
    target.pluck(*reflection.association_primary_key)
  elsif !target.empty?
    load_target.pluck(*reflection.association_primary_key)
  else
    @association_ids ||= scope.pluck(*reflection.association_primary_key)
  end
end

#ids_writer(ids)

Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 62

def ids_writer(ids)
  primary_key = reflection.association_primary_key
  pk_type = klass.type_for_attribute(primary_key)
  ids = Array(ids).compact_blank
  ids.map! { |id| pk_type.cast(id) }

  records = if klass.composite_primary_key?
    klass.where(primary_key => ids).index_by do |record|
      primary_key.map { |primary_key| record._read_attribute(primary_key) }
    end
  else
    klass.where(primary_key => ids).index_by do |record|
      record._read_attribute(primary_key)
    end
  end.values_at(*ids).compact

  if records.size != ids.size
    found_ids = records.map { |record| record._read_attribute(primary_key) }
    not_found_ids = ids - found_ids
    klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids)
  else
    replace(records)
  end
end

#include?(record) ⇒ Boolean

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 258

def include?(record)
  klass = reflection.klass
  return false unless record.is_a?(klass)

  if record.new_record?
    include_in_memory?(record)
  elsif loaded?
    target.include?(record)
  else
    record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
    scope.exists?(record_id)
  end
end

#include_in_memory?(record) ⇒ Boolean (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 507

def include_in_memory?(record)
  if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
    assoc = owner.association(reflection.through_reflection.name)
    assoc.reader.any? { |source|
      target_reflection = source.send(reflection.source_reflection.name)
      target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record
    } || target.include?(record)
  else
    target.include?(record)
  end
end

#insert_record(record, validate = true, raise = false, &block) (private)

Do the relevant stuff to insert the given record into the association collection.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 377

def insert_record(record, validate = true, raise = false, &block)
  if raise
    record.save!(validate: validate, &block)
  else
    record.save(validate: validate, &block)
  end
end

#load_target

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 272

def load_target
  if find_target?
    @target = merge_target_lists(find_target, target)
  end

  loaded!
  target
end

#merge_target_lists(persisted, memory) (private)

We have some records loaded from the database (persisted) and some that are in-memory (memory). The same record may be represented in the persisted array and in the memory array.

So the task of this method is to merge them according to the following rules:

* The final array must not have duplicates
* The order of the persisted array is to be preserved
* Any changes made to attributes on objects in the memory array are to be preserved
* Otherwise, attributes should have the value found in the database
[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 335

def merge_target_lists(persisted, memory)
  return persisted if memory.empty?

  persisted.map! do |record|
    if mem_record = memory.delete(record)

      ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save - mem_record.class._attr_readonly).each do |name|
        mem_record._write_attribute(name, record[name])
      end

      mem_record
    else
      record
    end
  end

  persisted + memory.reject(&:persisted?)
end

#reader

Implements the reader method, e.g. foo.items for Foo.has_many :items

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 34

def reader
  ensure_klass_exists!

  if stale_target?
    reload
  end

  @proxy ||= CollectionProxy.create(klass, self)
  @proxy.reset_scope
end

#remove_records(existing_records, records, method) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 399

def remove_records(existing_records, records, method)
  catch(:abort) do
    records.each { |record| callback(:before_remove, record) }
  end || return

  delete_records(existing_records, method) if existing_records.any?
  @target -= records
  @association_ids = nil

  records.each { |record| callback(:after_remove, record) }
end

#replace(other_array)

Replace this collection with other_array. This will perform a diff and delete/add only records that have changed.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 242

def replace(other_array)
  other_array.each { |val| raise_on_type_mismatch!(val) }
  original_target = skip_strict_loading { load_target }.dup

  if owner.new_record?
    replace_records(other_array, original_target)
  else
    replace_common_records_in_memory(other_array, original_target)
    if other_array != original_target
      transaction { replace_records(other_array, original_target) }
    else
      other_array
    end
  end
end

#replace_common_records_in_memory(new_target, original_target) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 430

def replace_common_records_in_memory(new_target, original_target)
  common_records = intersection(new_target, original_target)
  common_records.each do |record|
    skip_callbacks = true
    replace_on_target(record, skip_callbacks, replace: true)
  end
end

#replace_on_target(record, skip_callbacks, replace:, inversing: false) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 457

def replace_on_target(record, skip_callbacks, replace:, inversing: false)
  if replace && (!record.new_record? || @replaced_or_added_targets.include?(record))
    index = @target.index(record)
  end

  catch(:abort) do
    callback(:before_add, record)
  end || return unless skip_callbacks

  set_inverse_instance(record)

  @_was_loaded = true

  yield(record) if block_given?

  if !index && @replaced_or_added_targets.include?(record)
    index = @target.index(record)
  end

  @replaced_or_added_targets << record if inversing || index || record.new_record?

  if index
    target[index] = record
  elsif @_was_loaded || !loaded?
    @association_ids = nil
    target << record
  end

  callback(:after_add, record) unless skip_callbacks

  record
ensure
  @_was_loaded = nil
end

#replace_records(new_target, original_target) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 418

def replace_records(new_target, original_target)
  delete(difference(target, new_target))

  unless concat(difference(new_target, target))
    @target = original_target
    raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
                          "new records could not be saved."
  end

  target
end

#reset

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 87

def reset
  super
  @target = []
  @replaced_or_added_targets = Set.new.compare_by_identity
  @association_ids = nil
end

#scope

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 298

def scope
  scope = super
  scope.none! if null_scope?
  scope
end

#size

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 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.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 209

def size
  if !find_target? || loaded?
    target.size
  elsif @association_ids
    @association_ids.size
  elsif !association_scope.group_values.empty?
    load_target.size
  elsif !association_scope.distinct_value && !target.empty?
    unsaved_records = target.select(&:new_record?)
    unsaved_records.size + count_records
  else
    count_records
  end
end

#transaction(&block) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 321

def transaction(&block)
  reflection.klass.transaction(&block)
end

#writer(records)

Implements the writer method, e.g. foo.items= for Foo.has_many :items

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/collection_association.rb', line 46

def writer(records)
  replace(records)
end