Provides an executor for the graphql gem which allows queries to be batched.
Installation
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install graphql-batch
Usage
Basic Usage
Schema Configuration
Require the library
Define a custom loader, which is initialized with arguments that are used for grouping and a perform method for performing the batch load.
class RecordLoader < GraphQL::Batch::Loader def initialize(model) @model = model end def perform(ids) @model.where(id: ids).each { |record| fulfill(record.id, record) } ids.each { |id| fulfill(id, nil) unless fulfilled?(id) } end end
Use GraphQL::Batch as a plugin in your schema after specifying the mutation
so that GraphQL::Batch can extend the mutation fields to clear the cache after
they are resolved (for graphql >= 1.5.0).
class MySchema < GraphQL::Schema query MyQueryType mutation MyMutationType use GraphQL::Batch end
For pre 1.5.0 versions:
MySchema = GraphQL::Schema.define do query MyQueryType GraphQL::Batch.use(self) end
Field Usage
The loader class can be used from the resolver for a graphql field by calling .for with the grouping arguments to get a loader instance, then call .load on that instance with the key to load.
field :product, Types::Product, null: true do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id) end
The loader also supports batch loading an array of records instead of just a single record, via load_many. For example:
field :products, [Types::Product, null: true], null: false do argument :ids, [ID], required: true end def products(ids:) RecordLoader.for(Product).load_many(ids) end
Although this library doesn't have a dependency on active record, the examples directory has record and association loaders for active record which handles edge cases like type casting ids and overriding GraphQL::Batch::Loader#cache_key to load associations on records with the same id.
Promises
GraphQL::Batch::Loader#load returns a Promise using the promise.rb gem to provide a promise based API, so you can transform the query results using .then
def product_title(id:) RecordLoader.for(Product).load(id).then do |product| product.title end end
You may also need to do another query that depends on the first one to get the result, in which case the query block can return another query.
def product_image(id:) RecordLoader.for(Product).load(id).then do |product| RecordLoader.for(Image).load(product.image_id) end end
If the second query doesn't depend on the first one, then you can use Promise.all, which allows each query in the group to be batched with other queries.
def all_collections Promise.all([ CountLoader.for(Shop, :smart_collections).load(context.shop_id), CountLoader.for(Shop, :custom_collections).load(context.shop_id), ]).then do |results| results.reduce(&:+) end end
.then can optionally take two lambda arguments, the first of which is equivalent to passing a block to .then, and the second one handles exceptions. This can be used to provide a fallback
def product(id:) # Try the cache first ... CacheLoader.for(Product).load(id).then(nil, lambda do |exc| # But if there's a connection error, go to the underlying database raise exc unless exc.is_a?(Redis::BaseConnectionError) logger.warn err.message RecordLoader.for(Product).load(id) end) end
Unit Testing
Your loaders can be tested outside of a GraphQL query by doing the batch loads in a block passed to GraphQL::Batch.batch. That method will set up thread-local state to store the loaders, batch load any promise returned from the block then clear the thread-local state to avoid leaking state between tests.
def test_single_query product = products(:snowboard) title = GraphQL::Batch.batch do RecordLoader.for(Product).load(product.id).then(&:title) end assert_equal product.title, title end
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Contributing
See our contributing guidelines for more information.
License
The gem is available as open source under the terms of the MIT License.