Skip to content

Latest commit

 

History

History
96 lines (77 loc) · 3.05 KB

lazy_execution.md

File metadata and controls

96 lines (77 loc) · 3.05 KB
layout doc_stub search title section desc
guide
false
true
Lazy Execution
Schema
Resolve functions can return "unfinished" results. GraphQL will defer finishing them until other fields have been resolved.

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" | api_doc }}
  • In resolve functions, return instances of the lazy-loading class

Lazy resolution can be {% internal_link "instrumented","/fields/instrumentation" %}.

Example: Batched Find

Here's a way to find many objects by ID using one database call, preventing N+1 queries.

  1. 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
  1. Connect the lazy resolve method
class MySchema < GraphQL::Schema
  # ...
  lazy_resolve(LazyFindPerson, :person)
end
  1. Return lazy objects from resolve
field :author, PersonType, null: true

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-batch provides a powerful, flexible toolkit for lazy resolution with GraphQL.
  • dataloader is more general promise-based utility for batching queries within the same thread.
  • batch-loader works with any Ruby code including GraphQL, no extra dependencies or primitives.