Class: ActiveRecord::Duplicator::AssociationTraversal

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/active_record/duplicator/association_traversal.rb,
sig/generated/active_record/duplicator/association_traversal.rbs

Overview

Breadth-first traversal of associations declared with preload-style syntax. Yields ActiveRecord::Relation objects in the order they would be needed to duplicate records level by level.

Example:

traversal = ActiveRecord::Duplicator::AssociationTraversal.new(
owner,
[items: :taggings],
)
traversal.each do |relation|
# 1st: Relation of items belonging to owner
# 2nd: Relation of taggings belonging to those items
end

Instance Method Summary collapse

Constructor Details

#initialize(record, associations) ⇒ AssociationTraversal

: (untyped record, untyped associations) -> void

Parameters:

  • record (Object)
  • associations (Object)


25
26
27
28
# File 'lib/active_record/duplicator/association_traversal.rb', line 25

def initialize(record, associations)
  @record = record
  @associations = associations
end

Instance Method Details

#build_relation(klass, reflection, records) ⇒ ActiveRecord::Relation

Resolves the intermediate through_reflection first, then replaces it with the source_reflection. For has_many/has_one it looks up children by matching the parent's primary key against the child's foreign key. For belongs_to it looks up the parent by matching the child's foreign key against the parent's primary key.

For composite primary keys the foreign_key / active_record_primary_key of a reflection is an Array of column names; the corresponding WHERE clause uses tuple syntax like where([:a, :b] => [[v1, v2], [v3, v4]]). : (Class, untyped, untyped) -> ActiveRecord::Relation

Parameters:

  • (Class)
  • (Object)
  • (Object)

Returns:

  • (ActiveRecord::Relation)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/active_record/duplicator/association_traversal.rb', line 83

def build_relation(klass, reflection, records)
  if reflection.through_reflection?
    records = build_relation(reflection.through_reflection.klass, reflection.through_reflection, records)
    reflection = reflection.source_reflection
  end

  if reflection.collection? || reflection.has_one?
    lookup_columns = Array.wrap(reflection.active_record_primary_key)
    filter_columns = Array.wrap(reflection.foreign_key)
  elsif reflection.belongs_to?
    lookup_columns = Array.wrap(reflection.foreign_key)
    filter_columns = Array.wrap(reflection.active_record_primary_key)
  else
    raise NotImplementedError, "reflection kind #{reflection.class.name} is not supported"
  end

  ids = collect_ids(records, lookup_columns)
  klass.where(where_key(filter_columns) => ids)
end

#collect_ids(records, columns) ⇒ Array[untyped]

Avoid instantiating ActiveRecord objects just to read ids: use pluck for Relations and map for plain arrays. For composite pk returns an Array of Arrays ([[v1, v2], ...]); for single-column pk returns a flat Array ([v1, v2, ...]). : (untyped, Array[Symbol | String]) -> Array

Parameters:

  • (Object)
  • (Array[Symbol | String])

Returns:

  • (Array[untyped])


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/active_record/duplicator/association_traversal.rb', line 124

def collect_ids(records, columns)
  if records.is_a?(ActiveRecord::Relation)
    if columns.length == 1
      records.pluck(columns.first)
    else
      records.pluck(*columns)
    end
  else
    records.map do |r|
      if columns.length == 1
        r.public_send(columns.first)
      else
        columns.map { |c| r.public_send(c) }
      end
    end
  end
end

#eachself #eachEnumerator[ActiveRecord::Relation, self]

: () { (ActiveRecord::Relation) -> void } -> self : () -> Enumerator[ActiveRecord::Relation, self]

Overloads:

  • #eachself

    Returns:

    • (self)
  • #eachEnumerator[ActiveRecord::Relation, self]

    Returns:

    • (Enumerator[ActiveRecord::Relation, self])

Yields:

Yield Parameters:

  • arg0 (ActiveRecord::Relation)

Yield Returns:

  • (void)


32
33
34
35
36
37
38
39
# File 'lib/active_record/duplicator/association_traversal.rb', line 32

def each(&)
  return enum_for(:each) unless block_given?

  Array.wrap(@associations).each do |association|
    traverse(@record.class, association, Array.wrap(@record), &)
  end
  self
end

#find_reflection(klass, association) ⇒ Object

: (Class, Symbol | String) -> untyped

Parameters:

  • (Class)
  • (Symbol, String)

Returns:

  • (Object)


104
105
106
107
108
109
# File 'lib/active_record/duplicator/association_traversal.rb', line 104

def find_reflection(klass, association)
  reflection = klass.reflect_on_association(association)
  raise ArgumentError, "#{klass.name} has no association #{association}" if reflection.nil?

  reflection
end

#traverse(klass, association, records) {|arg0| ... } ⇒ void

This method returns an undefined value.

: (Class, untyped, untyped) { (ActiveRecord::Relation) -> void } -> void

Parameters:

  • (Class)
  • (Object)
  • (Object)

Yields:

Yield Parameters:

  • arg0 (ActiveRecord::Relation)

Yield Returns:

  • (void)


44
45
46
47
48
49
50
51
52
53
# File 'lib/active_record/duplicator/association_traversal.rb', line 44

def traverse(klass, association, records, &)
  case association
  when Hash
    traverse_hash(klass, association, records, &)
  when Symbol, String
    traverse_leaf(klass, association, records, &)
  else
    raise ArgumentError, "#{association.inspect} is not a valid association form"
  end
end

#traverse_hash(klass, association, records) {|arg0| ... } ⇒ void

This method returns an undefined value.

: (Class, Hash[Symbol | String, untyped], untyped) { (ActiveRecord::Relation) -> void } -> void

Parameters:

  • (Class)
  • (Hash[Symbol | String, untyped])
  • (Object)

Yields:

Yield Parameters:

  • arg0 (ActiveRecord::Relation)

Yield Returns:

  • (void)


62
63
64
65
66
67
68
69
70
71
72
# File 'lib/active_record/duplicator/association_traversal.rb', line 62

def traverse_hash(klass, association, records, &)
  association.each do |parent, children|
    reflection = find_reflection(klass, parent)
    relation = build_relation(reflection.klass, reflection, records)
    yield relation

    Array.wrap(children).each do |child|
      traverse(relation.klass, child, relation, &)
    end
  end
end

#traverse_leaf(klass, association, records) {|arg0| ... } ⇒ void

This method returns an undefined value.

: (Class, Symbol | String, untyped) { (ActiveRecord::Relation) -> void } -> void

Parameters:

  • (Class)
  • (Symbol, String)
  • (Object)

Yields:

Yield Parameters:

  • arg0 (ActiveRecord::Relation)

Yield Returns:

  • (void)


56
57
58
59
# File 'lib/active_record/duplicator/association_traversal.rb', line 56

def traverse_leaf(klass, association, records)
  reflection = find_reflection(klass, association)
  yield build_relation(reflection.klass, reflection, records)
end

#where_key(columns) ⇒ Symbol, ...

Rails accepts either a scalar column name or an Array of column names as the LHS of a WHERE clause. For single-column pk we hand it a scalar so the generated SQL stays as WHERE col IN (...). : (Array[Symbol | String]) -> (Symbol | String | Array[Symbol | String])

Parameters:

  • (Array[Symbol | String])

Returns:

  • (Symbol, String, Array[Symbol | String])


115
116
117
# File 'lib/active_record/duplicator/association_traversal.rb', line 115

def where_key(columns)
  columns.length == 1 ? columns.first : columns
end