preface

Before we get into GraphQL, here’s a story:

Front end student: The customer list in the interface you returned to me is all ID, can you return detailed information

Back end classmate: you call the customer details interface according to the ID

Front end student: Can the interface carry customer details at present

Back end classmate: No

Back end classmate: (pawn)

This is often the case in real development, where the front and back end have friendly communication over interface fields, types, and so on. The front end always wants to get all the data at once, avoiding multiple requests, especially with sequential requests; The back end does not want to fetch data across different data sources and wants to keep each data source as a separate interface for easy maintenance.

GraphQL was created to solve these problems.

What is a GraphQL

GraphQL is both a query language for the API and a runtime for your data queries. GraphQL provides an easy-to-understand set of complete descriptions of the data in your API, enabling a client to get exactly what it needs without any redundancy, making it easier for the API to evolve over time, and for building powerful developer tools.

It has the following advantages:

  1. Accurately describe the desired data on the front end, no more, no less
  2. With a single request for multiple resources, the GraphQL query can not only retrieve the properties of the resource, but also further query along inter-resource references
  3. The type system ensures that the data is correct

Current difficulties

While GraphQL has a lot of value on the front end, the work of providing the GraphQL API is on the back end.

How to solve it?

Actually, GraphQL doesn’t limit where you can run it. You can put the GraphQL gateway layer in the browser. In this way, we don’t need backend cooperation and can quickly convert to back-end services when they mature.

In actual combat

GraphQL gateway

Suppose we have two interfaces, one is to get the order list and the other is to get customer details. Now let’s convert them to GraphQL.

First we need to define the GraphQL type

[Order] type Order {id: Int # customerId customerId: Int} # get /customers/{id} Customer type Customer {id: Int name: String code: String} type Query {order(page: Int, pageSize: Int): [Order] customer(id: Int): Customer }Copy the code

Then we define the GraphQL schema

// schema.ts
import { makeExecutableSchema } from "@graphql-tools/schema";
import typeDefs from './schema.graphqls'

const resolvers = {
  Query: {
    async order(obj: any, args: any, ctx: any, info: any) {
      const res = await fetch(`/orders? page=${args.page}&pageSize=${args.pageSize}`).then(res= > res.json);
      return res;
    },
    async customer(obj: any, args: any, ctx: any, info: any) {
      const res = await fetch(`/customer/${args.id}`).then(res= > res.json);
      returnres; }}};export default makeExecutableSchema({
  typeDefs: typeDefs,
  resolvers,
});
Copy the code

This step is exactly the same as creating the GraphQL schema on NodeJS. Then we need to expose the schema. Then we need to expose the schema.

  • In NodeJS, we can expose via HTTP:
// server.js
var express = require('express');
var { graphqlHTTP } = require('express-graphql');
var { buildSchema } = require('graphql');
 
// Create a Schema using GraphQL Schema Language
var schema = buildSchema(` type Query { hello: String } `);
 
// root provides the corresponding parser functions for all API entry endpoints
var root = {
  hello: () = > {
    return 'Hello world! '; }};var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,})); app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');
Copy the code
  • In the browser, we expose through interfaces
// graphqlClient.ts
import schema from "./schema";
import { graphql } from "graphql";

export const graphqlClient = {
  async query(source: string) {
    const result = await graphql({ schema, source });
    returnresult; }};Copy the code
// Query the order
graphqlClient.query(
`query getOrder{ order(page: 1, pageSize: 20) { id, customerId } }`)
Copy the code

Extended order type

We know that one of the strengths of GraphQL is that it combines multiple requests into a single request and can be further queried through the resource’s field properties. We will add the Customer type on the Order to get the Customer name for the Order.

# schema.graphqls
...
extend type Order {
  customer: Customer
}
Copy the code
// schema.ts
const resolvers = {
  Query: {...Order: {
     async customer: (order: any) {
        const res = await fetch(`/customer/${order.customerId}`).then(res= > res.json);
        returnres; }}}};Copy the code
// Query the order
graphqlClient.query(
`query getOrder{ order(page: 1, pageSize: 20) { id, customerId, customer { id name } } }`)

Copy the code

N + 1 problem

When we perform an order query, we return 20 orders, and then we need to call the customer query interface 20 times, for a total of 20+1 calls. As we know, GraphqlQL will query along the link order-> Customer -> XXX. If the link is very long, the GraphQL response will be slow.

The current common GraphQL solution is the Dataloader provided by Facebook, whose core ideas are Batch Query and Cached. Suppose we have an interface that queries all customers /customers? Through a list of ids. Id = 123456789

// schema.ts
import DataLoader from "dataloader";
import keyBy from "lodash/fp/keyBy"

async function batchGetCustomers(keys: string[]) {
  const result = await fetch("/customers? id=" + keys.join(",")).then(res= > res.json());
  // Convert an array to a dictionary
  // [{ id: 1, name: "foo" }, { id: 2, name: "bar" }] 
  // => { "1": { id: 1, name: "foo" }, "2": { id: 2, name: "bar" } }
  const resultMap = keyBy("id", result);
  // The key must correspond to the object one by one. If it is null, return null
  return keys.map((x) = > resultMap[x] || null); 
}

const customerLoader = new DataLoader(batchGetCustomers);


const resolvers = {
  Query: {...Order: {
     async customer: (order: any) {
        return customerLoader.load(order.customerId)
     }
   }
  }
};
Copy the code

Eventually we reduced the query from 20+1 to 1+1

conclusion

Although we solved the problem of organizing complex data, it was hard to avoid various performance problems. In future plans, we will go further:

  1. Use a separate back end to provide the GraphQL gateway service
  2. Use a persistence layer to solve the data caching problem
  3. Organization of arbitrary data with low code