Edit Page

GraphQL API

The N+1 problem

It’s well known that every GraphQL service suffers of N+1 requests problem.

In our case this problem arises every time that a relation is mapped through the $fk operator with a MongoDB query.

For example, given the following GraphQL schema:

type User {
  id: Int!
  name: String
  posts: [Post]
}

type Post {
  id: Int!
  text: String
  author: User
}

type Query {
  posts(_limit: Int = 0, _skip: Int = 0): [Post]
}

mapped with:

{
  "descriptor": "...",
  "schema": "...",
  "mappings": {
    "User": {
      "posts": {
        "db": "restheart",
        "collection": "posts",
        "find": {
          "_id": {
            "$in": {
              "$fk": "posts_ids"
            }
          }
        }
      }
    },
    "Post": {
      "author": {
        "db": "restheart",
        "collection": "user",
        "find": {
          "_id": {
            "$fk": "author_id"
          }
        }
      }
    },
    "Query": {
      "posts": {
        "db": "restheart",
        "collection": "posts",
        "limit": {
          "$arg": "_limit"
        },
        "skip": {
          "$arg": "_skip"
        }
      }
    }
  }
}

then, executing the GraphQL query:

{ posts(_limit: 10) {
    text
    author {
      name
    }
  }
}

RESTHeart will execute:

  • a MongoDB query to fetch the first 10 documents of posts collection;

  • a MongoDB query for each one of the 10 posts returned by the first one.

Precisely, N+1 requests.

Batching and Caching

In order to mitigate the N+1 problem and optimize performances of yours GraphQL API, RESTHeart allows you to use per-request DataLoaders to batch and cache MongoDB queries. This can be done specifying dataLoader object within the Field to Query mapping. For instance, the author mapping seen above becomes:

{
  "descriptor": "...",
  "schema": "...",
  "mappings": {
    "User": "...",
    "Post": {
      "author": {
        "db": "restheart",
        "collection": "user",
        "find": {
          "_id": {
            "$fk": "author_id"
          }
        },
        "dataLoader": {
          "batching": true,
          "caching": true,
          "maxBatchSize": 20
        }
      }
    },
    "Query": "..."
  }
}

where:

  • batching (boolean): allows you to specify if queries batching is enabled. By default it’s false;

  • caching (boolean): allows you to specify if queries caching is enabled. By default it’s false;

  • maxBatchSize (int): allows you to specify how many queries batch together at most.

Tuning

There’s no magic number for maxBatchSize, so you have to tune it.

For this purpose you can set verbose: true under the GraphQL plugin configuration within restheart.yml. In this way, RESTHeart will insert DataLoader statistics inside GraphQL queries answers.

{
"data":  {"..."},
  "extensions":  {
  "dataloader":  {
      "overall-statistics":  {
        "loadCount":  0,
        "loadErrorCount":  0,
        "loadErrorRatio":  0.0,
        "batchInvokeCount":  0,
        "batchLoadCount":  0,
        "batchLoadRatio":  0.0,
        "batchLoadExceptionCount":  0,
        "batchLoadExceptionRatio":  0.0,
        "cacheHitCount":  0,
        "cacheHitRatio":  0.0
      },
      "individual-statistics":  {
        "dataLoader1":"...",
        "dataLoader2":"...",
        "dataLoader3":"..."
      }
    }
  }
}