Lazy Execution
With lazy execution, you can optimize access to external services (such as databases) by making batched calls. Building a lazy loader has three steps:
-
Define a lazy-loading class with one method for loading & returning a value
-
Connect it to your schema with
GraphQL::Schema.lazy_resolve -
In
resolvemethods, return instances of the lazy-loading class
Example: Batched Find
Hereβs a way to find many objects by ID using one database call, preventing N+1 queries.
-
Lazy-loading class which finds models by ID.
class LazyFindPerson def initialize(query_ctx, person_id) @person_id = person_id # Initialize the loading state for this query, # or get the previously-initiated state @lazy_state = query_ctx[:lazy_find_person] ||= { pending_ids: Set.new, loaded_ids: {}, } # Register this ID to be loaded later: @lazy_state[:pending_ids] << person_id end # Return the loaded record, hitting the database if needed def person # Check if the record was already loaded: loaded_record = @lazy_state[:loaded_ids][@person_id] if loaded_record # The pending IDs were already loaded, # so return the result of that previous load loaded_record else # The record hasn't been loaded yet, so # hit the database with all pending IDs pending_ids = @lazy_state[:pending_ids].to_a people = Person.where(id: pending_ids) people.each { |person| @lazy_state[:loaded_ids][person.id] = person } @lazy_state[:pending_ids].clear # Now, get the matching person from the loaded result: @lazy_state[:loaded_ids][@person_id] end end
-
Connect the lazy resolve method
class MySchema < GraphQL::Schema # ... lazy_resolve(LazyFindPerson, :person) end
-
Return lazy objects from
resolve
field :author, PersonType def author LazyFindPerson.new(context, object.author_id) end
Now, calls to author will use batched database access. For example, this query:
{
p1: post(id: 1) { author { name } }
p2: post(id: 2) { author { name } }
p3: post(id: 3) { author { name } }
}
Will only make one query to load the author values.
Gems for batching
The example above is simple and has some shortcomings. Consider the following gems for a robust solution to batched resolution:
-
GraphQL::Dataloaderis a built-in, Fiber-based approach to batching. See the Dataloader guide for more information. -
graphql-batchprovides a powerful, flexible toolkit for lazy resolution withGraphQL. -
dataloaderis more general promise-based utility for batching queries within the same thread. -
batch-loaderworks with any Ruby code includingGraphQL, no extra dependencies or primitives.