Class: Pipeloader::Batch::BatchProxy

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/pipeloader/batch/batch_proxy.rb

Overview

A lazy, chainable stand-in for a has_many association whose LOAD is batched across all live siblings of the owner. Query builders (where/order/limit/ select/…) accumulate an ActiveRecord::Relation; materializing (each/to_a/ map/…) runs ONE query for every sibling, applies the accumulated scope, partitions rows by foreign key, and slices limit/offset PER GROUP.

It duck-types the common Relation + Enumerable surface (~80% of a real CollectionProxy). Anything not handled here falls through to the real AR association, so writers (<<, build, create, …) still work.

Defined Under Namespace

Classes: WhereChain

Constant Summary collapse

QUERY_METHODS =

Pure query refinements that map 1:1 onto AR::Relation and return a new proxy. ‘where` is defined explicitly (below) so `where.not(…)` chains too.

%i[
  rewhere order reorder reselect distinct group having
  joins left_outer_joins includes preload eager_load references unscope readonly
].freeze
WRITE_METHODS =

The ONLY methods delegated to the real AR association. These are writes, so they can’t escape the (batched) read path. Everything not handled by this proxy raises NoMethodError instead of silently falling through to a per-record query.

%i[
  << concat push create create! build new
  delete destroy delete_all destroy_all clear replace
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(owner, reflection, relation: nil, limit_value: nil, offset_value: nil) ⇒ BatchProxy

Returns a new instance of BatchProxy.



33
34
35
36
37
38
39
# File 'lib/pipeloader/batch/batch_proxy.rb', line 33

def initialize(owner, reflection, relation: nil, limit_value: nil, offset_value: nil)
  @owner = owner
  @reflection = reflection
  @relation = relation
  @limit_value = limit_value
  @offset_value = offset_value
end

Instance Attribute Details

#limit_valueObject (readonly)

Returns the value of attribute limit_value.



41
42
43
# File 'lib/pipeloader/batch/batch_proxy.rb', line 41

def limit_value
  @limit_value
end

#offset_valueObject (readonly)

Returns the value of attribute offset_value.



41
42
43
# File 'lib/pipeloader/batch/batch_proxy.rb', line 41

def offset_value
  @offset_value
end

#ownerObject (readonly)

Returns the value of attribute owner.



41
42
43
# File 'lib/pipeloader/batch/batch_proxy.rb', line 41

def owner
  @owner
end

Instance Method Details

#[](*args) ⇒ Object



124
125
126
# File 'lib/pipeloader/batch/batch_proxy.rb', line 124

def [](*args)
  records[*args]
end

#cache_signatureObject

Signature of the accumulated scope (without the per-owner FK filter), so siblings asking for the same scope share one batch.



150
151
152
# File 'lib/pipeloader/batch/batch_proxy.rb', line 150

def cache_signature
  [relation.to_sql, @limit_value, @offset_value]
end

#each(&block) ⇒ Object



98
99
100
# File 'lib/pipeloader/batch/batch_proxy.rb', line 98

def each(&block)
  records.each(&block)
end

#empty?Boolean

Returns:

  • (Boolean)


116
117
118
# File 'lib/pipeloader/batch/batch_proxy.rb', line 116

def empty?
  records.empty?
end

#exists?(*args, **kwargs) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
145
146
# File 'lib/pipeloader/batch/batch_proxy.rb', line 142

def exists?(*args, **kwargs)
  return !empty? if args.empty? && kwargs.empty?

  where(*args, **kwargs).any?
end

#find_by(*args, **kwargs) ⇒ Object

Batched, under our control: both reuse the batched scope rather than issuing a per-owner query.



138
139
140
# File 'lib/pipeloader/batch/batch_proxy.rb', line 138

def find_by(*args, **kwargs)
  where(*args, **kwargs).first
end

#foreign_keyObject



47
48
49
# File 'lib/pipeloader/batch/batch_proxy.rb', line 47

def foreign_key
  @reflection.foreign_key.to_s
end

#idsObject



128
129
130
# File 'lib/pipeloader/batch/batch_proxy.rb', line 128

def ids
  records.map(&:id)
end

#inspectObject



154
155
156
# File 'lib/pipeloader/batch/batch_proxy.rb', line 154

def inspect
  records.inspect
end

#last(*args) ⇒ Object



120
121
122
# File 'lib/pipeloader/batch/batch_proxy.rb', line 120

def last(*args)
  records.last(*args)
end

#limit(value) ⇒ Object

limit/offset are honored PER GROUP (top-N per owner), applied after the single batched query — not as a global SQL LIMIT that would collapse every owner into one window.



74
75
76
# File 'lib/pipeloader/batch/batch_proxy.rb', line 74

def limit(value)
  spawn(limit_value: value)
end

#nameObject



43
44
45
# File 'lib/pipeloader/batch/batch_proxy.rb', line 43

def name
  @reflection.name
end

#offset(value) ⇒ Object



78
79
80
# File 'lib/pipeloader/batch/batch_proxy.rb', line 78

def offset(value)
  spawn(offset_value: value)
end

#owner_keyObject



51
52
53
# File 'lib/pipeloader/batch/batch_proxy.rb', line 51

def owner_key
  @reflection.active_record_primary_key.to_sym
end

#pluck(*columns) ⇒ Object



132
133
134
# File 'lib/pipeloader/batch/batch_proxy.rb', line 132

def pluck(*columns)
  records.map { |record| columns.one? ? record[columns.first] : columns.map { |column| record[column] } }
end

#recordsObject Also known as: to_a, to_ary, load



102
103
104
105
106
# File 'lib/pipeloader/batch/batch_proxy.rb', line 102

def records
  return @records if defined?(@records)

  @records = Pipeloader::Batch::BatchLoader.load(self)
end

#relationObject



55
56
57
# File 'lib/pipeloader/batch/batch_proxy.rb', line 55

def relation
  @relation ||= apply_reflection_scope(@reflection.klass.all)
end

#select(*columns, &block) ⇒ Object

select(:cols) refines the query; select { block } filters loaded records (matching ActiveRecord::Relation#select’s dual behavior).



92
93
94
95
96
# File 'lib/pipeloader/batch/batch_proxy.rb', line 92

def select(*columns, &block)
  return records.select(&block) if block

  spawn(relation: relation.select(*columns))
end

#sizeObject Also known as: length



111
112
113
# File 'lib/pipeloader/batch/batch_proxy.rb', line 111

def size
  records.size
end

#where(*args, **kwargs, &block) ⇒ Object

where(conditions) refines the batched query; bare ‘where` returns a chain so `where.not(…)` works like a relation’s.



84
85
86
87
88
# File 'lib/pipeloader/batch/batch_proxy.rb', line 84

def where(*args, **kwargs, &block)
  return WhereChain.new(self) if args.empty? && kwargs.empty? && block.nil?

  spawn(relation: relation.where(*args, **kwargs, &block))
end