Documentation
¶
Overview ¶
Package dataloader coalesces N individual key lookups into one batched fetch, eliminating the N+1 query pattern that GraphQL's nested resolvers otherwise produce.
Two pieces:
Loader[K, V] — holds a fetch function and per-request state. Load(key) enqueues the key and returns a thunk; the first thunk to run dispatches one batched fetch for every enqueued key.
Registry — per-request map of loaders, attached to context via WithRegistry. Get[K, V] looks up (or lazily creates) a named loader on the registry so multiple resolvers in one request share one Loader instance.
Wired into the GraphQL transport via a request middleware that drops a fresh Registry on every POST /graphql. graphql-go's thunk-aware executor calls deferred resolvers breadth-first after the synchronous pass, which is exactly the batching window the Loader exploits — no goroutines, no timeouts, no surprises.
Example, inside a GraphQL resolver:
loader := dataloader.Get[int64, *BankDetail](p.Context, "bankByUserID",
func(ctx context.Context, userIDs []int64) (map[int64]*BankDetail, error) {
return db.BankDetailsByUserIDs(ctx, userIDs)
})
thunk := loader.Load(user.ID)
return thunk, nil
50 users in a list query → 1 BankDetailsByUserIDs call.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Fetch ¶
type Fetch[K comparable, V any] func(ctx context.Context, keys []K) (map[K]V, error)
Fetch is the batched lookup the Loader memoizes. It receives every distinct key seen during one Loader's lifetime and must return a map keyed by those same keys. Missing keys in the returned map are surfaced as the zero V (the resolver decides whether that means "null" or "not found").
Errors propagate to every thunk attached to this loader instance — one fetch failure fails every caller in that request, which matches the dataloader-spec semantics and prevents partial-render confusion.
type Loader ¶
type Loader[K comparable, V any] struct { // contains filtered or unexported fields }
Loader is the per-(request, name) batcher. Construct via Get; do not create directly — the registry handles lifecycle so siblings share a single instance.
func Get ¶
Get returns the registered loader for (ctx, name), creating it on the first call within a request and reusing it on subsequent calls. The fetch function is captured on first call; second-and- later calls with different fetch functions silently get the first-registered one, which keeps siblings batching together.
When the registry isn't on ctx (no middleware), Get returns a brand-new Loader every call — batching still works within one resolver's scope, but cross-resolver sharing is lost. Convenient for tests, wrong for production: install the middleware.
func New ¶
func New[K comparable, V any](fetch Fetch[K, V]) *Loader[K, V]
New constructs a standalone Loader. Most callers should use dataloader.Get(ctx, name, fetch) instead — it shares one Loader across every resolver in the same request, which is the whole point of the pattern. New is here for tests + advanced callers that manage lifecycle by hand.
func (*Loader[K, V]) Load ¶
Load enqueues key for the next batch and returns a thunk that, when called, returns the value for that key. The thunk is intended to be returned directly from a graphql-go resolver — the executor dethunks breadth-first after all sibling resolvers have run, which is exactly when every key for this batch is in the queue.
Duplicate keys are deduped: 50 thunks for the same user.ID enqueue the key once and share the result. The caller doesn't have to pre-uniqueify upstream.
func (*Loader[K, V]) LoadCtx ¶
LoadCtx is Load with an explicit context for the batch call. Most resolvers pass the GraphQL request context; this overload exists so the batch call can use a different one (e.g. a longer-lived background ctx during cleanup). First call wins — siblings calling LoadCtx with different contexts get the first one for the batch.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the per-request map of loaders. The GraphQL transport's request middleware drops a fresh Registry on the context for every POST /graphql; resolvers reach into it via Get to share a single Loader instance with their siblings.
Methods are safe for concurrent use. A nil receiver makes Get return a one-shot Loader (no sharing) — useful in tests that don't install the middleware, but pointless in production.
func FromContext ¶
FromContext returns the registry stashed by WithRegistry, or nil if the middleware didn't run for this request. Most callers should use Get instead; FromContext is exposed for advanced use (e.g. manually priming several loaders from a top-level resolver).