123456789_123456789_123456789_123456789_123456789_

Class: ActiveRecord::Associations::HasManyThroughAssociation

Do not use. This class is for internal use only.

Overview

Active Record Has Many Through Association

Class Method Summary

Instance Attribute Summary

ThroughAssociation - Included

ForeignAssociation - Included

CollectionAssociation - Inherited

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

ThroughAssociation - Included

#build_record,
#construct_join_attributes

Construct attributes for :through pointing to owner and associate.

#ensure_mutable, #ensure_not_nested,
#stale_state

Note: this does not capture all cases, for example it would be impractical to try to properly support stale-checking for nested associations.

#target_scope

We merge in these scopes for two reasons:

#through_association, #through_reflection, #transaction

HasManyAssociation - Inherited

ForeignAssociation - Included

#nullified_owner_attributes,
#set_owner_attributes

Sets the owner attributes on the given record.

CollectionAssociation - Inherited

#add_to_target, #build,
#concat

Add records to this association.

#delete

Removes records from this association calling before_remove and after_remove callbacks.

#delete_all

Removes all records from the association without calling callbacks on the associated records.

#destroy

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

#destroy_all

Destroy all the records from this association.

#find,
#ids_reader

Implements the ids reader method, e.g.

#ids_writer

Implements the ids writer method, e.g.

#include?, #load_target,
#reader

Implements the reader method, e.g.

#replace

Replace this collection with other_array.

#reset, #scope,
#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.

#writer

Implements the writer method, e.g.

#_create_record, #callback, #callbacks_for, #concat_records, #delete_or_destroy,
#delete_records

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

#find_by_scan

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

#include_in_memory?,
#insert_record

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

#merge_target_lists

We have some records loaded from the database (persisted) and some that are in-memory (memory).

#remove_records, #replace_common_records_in_memory, #replace_on_target, #replace_records, #transaction

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

.new(owner, reflection) ⇒ HasManyThroughAssociation

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 9

def initialize(owner, reflection)
  super
  @through_records = {}.compare_by_identity
end

Instance Attribute Details

#target_reflection_has_associated_record?Boolean (readonly, private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 121

def target_reflection_has_associated_record?
  !(through_reflection.belongs_to? && Array(through_reflection.foreign_key).all? { |foreign_key_column| owner[foreign_key_column].blank? })
end

#through_scope (readonly, private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 69

attr_reader :through_scope

Instance Method Details

#build_record(attributes) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 90

def build_record(attributes)
  ensure_not_nested

  @through_scope = scope
  record = super

  inverse =
    if source_reflection.polymorphic?
      source_reflection.polymorphic_inverse_of(record.class)
    else
      source_reflection.inverse_of
    end

  if inverse
    if inverse.collection?
      record.send(inverse.name) << build_through_record(record)
    elsif inverse.has_one?
      record.send("#{inverse.name}=", build_through_record(record))
    end
  end

  record
ensure
  @through_scope = nil
end

#build_through_record(record) (private)

The through record (built with build_record) is temporarily cached so that it may be reused if insert_record is subsequently called.

However, after insert_record has been called, the cache is cleared in order to allow multiple instances of the same record in an association.

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 56

def build_through_record(record)
  @through_records[record] ||= begin
    ensure_mutable

    attributes = through_scope_attributes
    attributes[source_reflection.name] = record

    through_association.build(attributes).tap do |new_record|
      new_record.send("#{source_reflection.foreign_type}=", options[:source_type]) if options[:source_type]
    end
  end
end

#concat(*records)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 14

def concat(*records)
  unless owner.new_record?
    records.flatten.each do |record|
      raise_on_type_mismatch!(record)
    end
  end

  super
end

#concat_records(records) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 37

def concat_records(records)
  ensure_not_nested

  records = super(records, true)

  if owner.new_record? && records
    records.flatten.each do |record|
      build_through_record(record)
    end
  end

  records
end

#delete_or_nullify_all_records(method) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 136

def delete_or_nullify_all_records(method)
  delete_records(load_target, method)
end

#delete_records(records, method) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 140

def delete_records(records, method)
  ensure_not_nested

  scope = through_association.scope
  scope.where! construct_join_attributes(*records)
  scope = scope.where(through_scope_attributes)

  case method
  when :destroy
    if scope.model.primary_key
      count = scope.destroy_all.count(&:destroyed?)
    else
      scope.each(&:_run_destroy_callbacks)
      count = scope.delete_all
    end
  when :nullify
    count = scope.update_all(source_reflection.foreign_key => nil)
  else
    count = scope.delete_all
  end

  delete_through_records(records)

  if source_reflection.options[:counter_cache] && method != :destroy
    counter = source_reflection.counter_cache_column
    klass.decrement_counter counter, records.map(&:id)
  end

  if through_reflection.collection? && update_through_counter?(method)
    update_counter(-count, through_reflection)
  else
    update_counter(-count)
  end

  count
end

#delete_through_records(records) (private)

[ GitHub ]

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

def delete_through_records(records)
  records.each do |record|
    through_records = through_records_for(record)

    if through_reflection.collection?
      through_records.each { |r| through_association.target.delete(r) }
    else
      if through_records.include?(through_association.target)
        through_association.target = nil
      end
    end

    @through_records.delete(record)
  end
end

#difference(a, b) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 177

def difference(a, b)
  distribution = distribution(b)

  a.reject { |record| mark_occurrence(distribution, record) }
end

#distribution(array) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 193

def distribution(array)
  array.each_with_object(Hash.new(0)) do |record, distribution|
    distribution[record] += 1
  end
end

#find_target(async: false) (private)

Raises:

  • (NotImplementedError)
[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 225

def find_target(async: false)
  raise NotImplementedError, "No async loading for HasManyThroughAssociation yet" if async
  return [] unless target_reflection_has_associated_record?
  return scope.to_a if disable_joins
  super
end

#insert_record(record, validate = true, raise = false)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 24

def insert_record(record, validate = true, raise = false)
  ensure_not_nested

  if record.new_record? || record.has_changes_to_save?
    return unless super
  end

  save_through_record(record)

  record
end

#intersection(a, b) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 183

def intersection(a, b)
  distribution = distribution(b)

  a.select { |record| mark_occurrence(distribution, record) }
end

#invertible_for?(record) ⇒ Boolean (private)

NOTE - not sure that we can actually cope with inverses here

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 233

def invertible_for?(record)
  false
end

#mark_occurrence(distribution, record) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 189

def mark_occurrence(distribution, record)
  distribution[record] > 0 && distribution[record] -= 1
end

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

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 116

def remove_records(existing_records, records, method)
  super
  delete_through_records(records)
end

#save_through_record(record) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 81

def save_through_record(record)
  association = build_through_record(record)
  if association.changed?
    association.save!
  end
ensure
  @through_records.delete(record)
end

#through_records_for(record) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 199

def through_records_for(record)
  attributes = construct_join_attributes(record)
  candidates = Array.wrap(through_association.target)
  candidates.find_all do |c|
    attributes.all? do |key, value|
      c.public_send(key) == value
    end
  end
end

#through_scope_attributes (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 71

def through_scope_attributes
  scope = through_scope || self.scope
  attributes = scope.where_values_hash(through_association.reflection.klass.table_name)
  except_keys = [
    *Array(through_association.reflection.foreign_key),
    through_association.reflection.klass.inheritance_column
  ]
  attributes.except!(*except_keys)
end

#update_through_counter?(method) ⇒ Boolean (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/associations/has_many_through_association.rb', line 125

def update_through_counter?(method)
  case method
  when :destroy
    !through_reflection.inverse_updates_counter_cache?
  when :nullify
    false
  else
    true
  end
end