Rails Kitchen

It's a place to write on stuff I learned recently.

Solving N+1 Query in GraphQL Using Graphql-batch

| Comments

One of the most important pain points in GraphQL is the problem thats commonly referred to as N+1 SQL queries. GraphQL query fields are designed to be stand-alone functions, and resolving those fields with data from a database might result in a new database request per resolved field.

For a simple RESTful API endpoint logic, it’s easy to analyze, detect, and solve N+1 issues by enhancing the constructed SQL queries. For GraphQL dynamically resolved fields, it’s not that simple.

For example, Consider the GraphQL query to fetch article, its comments and commented user which I mentioned in previous post.
1
2
3
4
5
6
7
8
9
10
11
12
query {
  acticle(id: 1){
    title
    comments{
      comment
      user {
        id
        name
      }
    }
  }
}
Output Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
  "data": {
    "acticle": {
      "title": "A GraphQL Server",
      "comments": [
        {
          "comment": "Good article",
          "user": {
            "id": 1,
            "name": "Shaiju E"
          }
        },
        {
          "comment": "Keep going",
          "user": {
            "id": 1,
            "name": "Shaiju E"
          }
        },
        {
          "comment": "Another Comment",
          "user": {
            "id": 2,
            "name": "David"
          }
        },
        {
          "comment": "New Comment",
          "user": {
            "id": 1,
            "name": "Shaiju E"
          }
        },
        {
          "comment": "Another Comment from User 2",
          "user": {
            "id": 2,
            "name": "David"
          }
        },
        {
          "comment": "Another Comment from User 1",
          "user": {
            "id": 1,
            "name": "Shaiju E"
          }
        },
        {
          "comment": "TEST",
          "user": {
            "id": 1,
            "name": "Shaiju E"
          }
        }
      ]
    }
  }
}
This will fire user query in for each comment. Total 7 instead of 1 for fetch 2 user. In Rest API we can solve this issue by eager loading users while fetching comments. But GraphQL query fields are designed to be stand-alone functions, and not aware of other functions.

Facebook introduced DataLoader to solve this problem in Javascript projects. Shopify created GraphQL::Batch to solve this N+1 problem in ruby.
GraphQL::Batch Provides an executor for the graphql gem which allows queries to be batched. This is a flexible toolkit for lazy resolution with GraphQL.

Installation
1
gem 'graphql-batch'
Now we need to define a custom loader, which is initialized with arguments that are used for grouping and a perform method for performing the batch load.
app/graphql/record_loader.rb
1
2
3
4
5
6
7
8
9
10
11
require 'graphql/batch'
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
Now we need to GraphQL::Batch as a plugin in your schema
app/graphql/graphql_ruby_sample_schema.rb
1
2
3
4
5
6
GraphqlRubySampleSchema = GraphQL::Schema.define do
  query Types::QueryType
  mutation Mutations::MutationType

  use GraphQL::Batch
end
In our comments api example, we need to use above initialized RecordLoader for lazily execute User query
app/graphql/types/comment_type.rb
1
2
3
4
5
6
7
8
9
10
11
12
module Types
  CommentType = GraphQL::ObjectType.define do
    name "Comment"
    field :id, types.Int
    field :comment, types.String
    field :user, -> { UserType } do
      resolve -> (obj, args, ctx) {
        RecordLoader.for(User).load(obj.user_id)
      }
    end
  end
end
Here, resolve -> (obj, args, ctx) {RecordLoader.for(User).load(obj.user_id) } will make user fetching lazy loading there by solve N+1 query problem. Before: After

More information about GraphQL::Batch is available in Gem Documentation
You can see sample code here.

Comments