NAME
GraphQL::Houtou::DataLoader - batching loader for GraphQL::Houtou resolvers
SYNOPSIS
use GraphQL::Houtou qw(execute);
use GraphQL::Houtou::DataLoader;
# per request
my $users = GraphQL::Houtou::DataLoader->new(batch => sub {
my ($ids) = @_;
my %row = map { $_->{id} => $_ } $db->select_users_in(@$ids);
return [ map { $row{$_} } @$ids ]; # one entry per key, key order
});
# resolvers return promises from the loader
# resolve => sub { my ($src) = @_; $users->load($src->{user_id}) }
my $result = execute($schema, $query, $variables,
context => { users => $users },
on_stall => GraphQL::Houtou::DataLoader->on_stall_for($users),
);
DESCRIPTION
Solves the N+1 problem for SQL-backed resolvers. Every load during one execution phase queues its key and returns a promise; when execution stalls (all remaining fields are waiting on promises), the on_stall hook dispatches the queued keys to the batch function in a single call and settles the promises, which resumes execution. A query touching N users issues one batched lookup per nesting level instead of N individual ones.
execute with on_stall drives the request to completion and returns the finished response synchronously - callers never handle promises.
MULTIPLE LOADERS
Real schemas use several loaders per request, and one loader is usually shared by every field that resolves the same kind of record. Both shapes need no special wiring: put every loader in the context and register them all with on_stall_for:
my $users = GraphQL::Houtou::DataLoader->new(batch => \&batch_users);
my $entries = GraphQL::Houtou::DataLoader->new(batch => \&batch_entries);
# Blog.author and Entry.author share $users; Blog also uses $entries.
# Blog: author => sub { $_[2]->{users}->load($_[0]{author_id}) }
# latestEntry => sub { $_[2]->{entries}->load($_[0]{latest_entry_id}) }
# Entry: author => sub { $_[2]->{users}->load($_[0]{author_id}) }
my $result = execute($schema, $query, $variables,
context => { users => $users, entries => $entries },
on_stall => GraphQL::Houtou::DataLoader->on_stall_for($users, $entries),
);
Sharing one loader across types means the per-request cache dedupes keys globally: a user already fetched for Blog.author is not fetched again for Entry.author. Loaders may also feed each other - settling one loader's promises can queue loads on another, and a single stall keeps dispatching until a full pass makes no progress - so each dependency level still costs one batched query per loader.
THE BATCH FUNCTION
Receives an arrayref of unique keys and must return an arrayref of the same length, in key order. An entry may be a GraphQL::Houtou::DataLoader::Error object to fail only that key; a die inside the batch function fails every key in the batch.
METHODS
load($key), load_many(\@keys), prime($key, $value), clear($key), clear_all, pending_count, dispatch.
load_many follows dataloader-js loadMany: it takes an arrayref of keys and returns a single promise that resolves with an arrayref of values in key order. It never rejects on per-key failures - failed slots hold GraphQL::Houtou::DataLoader::Error objects (check with blessed + isa, read the reason with ->message). Calling it with a flat key list is deprecated (it returns one promise per key and warns in the deprecated category).
Instances cache per key (create one loader per request unless you want cross-request caching). Pass cache => 0 to disable, cache_key to derive cache keys from structured keys, max_batch_size to chunk large batches.
THE on_stall CONTRACT
This loader is one implementation of the generic batching contract: the on_stall callback passed to execute is invoked whenever execution cannot proceed because promises are pending, and must return the number of units it dispatched. Returning 0 while promises remain pending is reported as a deadlock. Anything that can resolve promises - a hand-written batcher, an ORM-specific loader - can implement the same contract; GraphQL::Houtou::DataLoader->on_stall_for(@loaders) builds the callback for one or more of these loaders.