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
-
.batch_fill(association) ⇒ Object
The singular interceptor’s hook: only fire for an association this owner has batch-declared, then defer to fill_association.
-
.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.
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 |