Module: Pipeloader::Batch

Defined in:
lib/pipeloader/batch.rb,
lib/pipeloader/batch/model.rb,
lib/pipeloader/batch/context.rb,
lib/pipeloader/batch/fetcher.rb,
lib/pipeloader/batch/batch_proxy.rb,
lib/pipeloader/batch/batch_loader.rb,
lib/pipeloader/batch/relationship.rb,
lib/pipeloader/batch/fetcher_state.rb,
lib/pipeloader/batch/load_grouping.rb,
lib/pipeloader/batch/load_interceptor.rb

Overview

Automatic N+1 elimination for plain ActiveRecord — no GraphQL, no ‘includes`, no DataLoader keys. Declare a relationship with a batch macro, then traverse records one at a time; the first access loads the relationship for every record loaded alongside it (its sibling group) in a single query.

class Author < ApplicationRecord
  include Pipeloader::Batch::Model
  batch_has_many :books            # chainable, batched (where/order/limit)
  batch_has_one  :profile
  batch_count    :books_count
end

Author.all.to_a.each { |a| a.books.to_a }   # ONE query for everyone's books

Siblings are the records loaded by the same query: the group is stamped onto them as they load (Pipeloader::Batch::LoadGrouping) and carried on the records, so it needs no surrounding block and is correct under threads, fibers, and fiber-per-request servers alike.

Defined Under Namespace

Modules: BatchLoader, LoadGrouping, LoadInterceptor, Model Classes: BatchProxy, Context, Error, Fetcher, FetcherState, Relationship

Class Method Summary collapse

Class Method Details

.batch_fill(association) ⇒ Object

The singular interceptor’s hook: only fire for an association this owner has batch-declared, then defer to fill_association.



19
20
21
22
23
24
25
# File 'lib/pipeloader/batch/load_interceptor.rb', line 19

def self.batch_fill(association)
  klass = association.owner.class
  return unless klass.respond_to?(:pipeloader_batched_association?)
  return unless klass.pipeloader_batched_association?(association.reflection.name)

  fill_association(association.owner, association.reflection.name)
end

.fill_association(owner, name) ⇒ Object

Preload ‘name` across every live sibling of `owner` in one shot, so each sibling’s target is set without a per-record query. Uses AR’s own Preloader, which walks plain, polymorphic, and :through associations alike. After this, the caller’s ‘load_target` finds the target loaded and returns it.



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/pipeloader/batch/load_interceptor.rb', line 31

def self.fill_association(owner, name)
  return if Context.preloading?

  siblings = owner._pipeloader_batch_context.all(owner.class)
  records = siblings.select { |record| record.persisted? && !record.association(name).loaded? }
  records << owner if owner.persisted? && records.none? { |record| record.equal?(owner) }
  return if records.empty?

  Context.while_preloading do
    ::ActiveRecord::Associations::Preloader.new(records: records, associations: name).call
  end
end