GraphQL interface design

GraphQL interface design

Please refer to my blog for the original article: GraphQL interface design

graphql is a query language for APIs. It provides a complete and easy-to-understand API interface data description, giving clients the power to accurately query the data they need without having to implement more codes, making API interface development easier and more efficient.

Recently GatsbyI am developing a version of static blog, and I feel that I really hate seeing this framework. Because this framework uses graphql technology, I took some time to learn and record the learning and thinking process.

This article mainly explains how to understand graphql and the idea of designing graphql based on relational databases. If you need to learn the basics of graphql, please move to the official document

This article contains Nestjsa sample project of + graphql: github.com/YES-Lee/nes...

Understand graphql

Graphql is a query language used to describe data and its relationships. The graphql standard is described in official documents. The specific implementation depends on a third party. The current mainstream is Apollothe solution provided.

Graphql does not care about how we obtain the data, we only need to provide a method for obtaining the data (resolver) and how to assemble the data (schema). Similar to the interface design pattern in the Java development process, Graphql defines a set of standards, and we implement interfaces according to the standards. Let's take the user-role model as an example.

The following code defines two data structures, which are similar to JSON and should be easy to understand. It describes the name of each type (type) and the attributes it contains. In addition to basic types, attributes can also be arrays and other reference types, thereby establishing all data models and mutual relationships.

type Role {
  name: String
  note: String
}
type User {
  id: Int
  name: String
  gender: Int
  role: Role
}
 

It used to describe the above code Roleand Userdata structures specific to how we use this thing? From the front-end point of view, you can learn the basic use of the front-end from official documents. The data description in the request body is slightly different from the code defined above. For example, we want to query user data:

query userInfo {
  user(id: Int) {
    id
    name
    gender
    role {
      name
      note
    }
  }
}
 

From the above code, you can probably guess that if we don t need to query the roledata, we just need to remove it from the request

query userInfo {
  user(id: Int) {
    id
    name
    gender
  }
}
 

When the request arrives at the server, graphql will parse the request body. When it is parsed user, it will execute userthe logic we defined to get the data role. The same is true for parsing . Then we have to define the logic for obtaining data on the server, which is called in graphql resolver.

The previous code only defines the structure of the data, we also need to create one resolverto obtain the corresponding data, similar to the following code

function userResolver (id) {
  //...
  //return User
}

function roleResolver (userId) {
  //...
  //return Role
}
 

We can execute any logic that can obtain the required data, such as sql, http request, rpc communication, etc. in the resolver. In the end, you only need to return the data according to the predefined structure.

Schema

The codes used to define different types of data structures are called Schema. It is graphql language used to describe the relationship between the data structure and, in Schema-defined type may be used Int, Float, String, Boolean, IDetc. graphql defined scalar types (Scalar Types), may also be used in combination type, and the like refer to another type, such as UserReferenced in the code above Role.

Resolver

ResolverIt is the method used to get data in GraphQL. It does not care about Resolverthe specific implementation, we can get data from the database, HTTP interface, server resources and other channels.

Graphql will execute the resolver according to the fields declared in the request, which can reduce the number of queries to a certain extent. In the following code, it will be executed UserResolverand will continue to execute after the endRoleResolver

{
  User {
    name
    role
  }
}
 

If we remove the role field, the server will no longer executeRoleResolver

{
  User {
    name
  }
}
 

UserResolverAfter the execution is over, the data is returned to the front end.

It can be seen that graphql can dynamically perform data queries, reducing unnecessary resource consumption. However, everything has both sides. From another perspective, how do we control the granularity of the Resolver? Assuming that our Resolver is to perform database query, in restful API, we usually use a relational query to obtain Userand Roletwo data at the same time . But in graphql, we create a Resolver for each associated object. When we query a user list and need to include user-related roles, we will find a problem. One request needs to execute N+1 SQL queries: UserResolver obtains N user lists once, and N queries obtain the roles of each user. This is the N+1 problem that we need to talk about later.

Problems with graphql

N+1 problem

N+1The problem is not only in graphql. When we write sql, there will be a similar situation, but we will avoid this problem through associated queries.

In order to solve the N+1 problem, graphql officially provides a dataloadersolution. Dataloader uses caching technology, and it also merges multiple identical (similar) requests, combining the N queries mentioned above into one. The basic principle is to perform the operation of querying the list first, and then use the associated field of each record as a parameter list, and after obtaining all the associated data through a query, it will be merged into the superior data. Dataloader is currently a more effective method to solve the N+1 problem, and there is not much difficulty in use.

2. we can reduce the number of queries by controlling the granularity of the Resolver. For example, in the previous example, it is not written roleResolver, but directly through the associated query, Userand get the sum with one query Role. Of course, if you do this role, the service will perform an associated query regardless of whether the front-end queries the field or not . Here you need to choose according to specific scenarios.

HTTP cache problem

Since the graphql interface request has only one unified endpoint, we cannot use the HTTPcache. Some current front-end implementations, such as Apollo, provide inMemeryCachesolutions, but the user experience is not very friendly.

Relational database + graphql interface design

After roughly understanding graphql, let's start the API design of relational database (Mysql) + graphql

Write Schema

For schemathe design, first ignore the relationship between the tables, and first establish a model corresponding to the data table. Such as Usertables and Roletables, respectively, are established as followsschema

type User {
  name: String
  gender: Int
  # password: String// 
}
type Role {
  name: String
  note: String
}
 

It should be noted that sensitive data should not appear in sensitive information (user passwords, etc.), even if the result of the Resolver contains these sensitive information, as long as schemait does not contain it, graphql will automatically filter these fields.

After establishing all the table correspondences schema, consider the relationship between the tables.

Table relationship processing

Graphql has some differences between the processing of relations and Restful API. In Restful API, we generally only establish relational queries in the required interfaces. We will do some SQL optimizations for the interfaces, in order to quickly query what we need in a piece of SQL. All information.

But in graphql, we should describe the relationship of all tables as a graph structure to ensure that the schemas corresponding to all tables with a relationship (one-to-many or many-to-many) are all together, so that when we request it, we can From a node to any node related to it.

This is also one of the charms of graphql. When we build a complete relationship diagram, the front end can freely query and combine data. In theory, the front end can query a set of data infinitely recursively, such as: Xiao Ming -> Xiao Ming's friends -> Xiao Ming's friends ->..., we only need to choose a starting point, and we can reach anywhere.

One-to-many relationship

The establishment of a one-to-many relationship is very simple, we only need to write the corresponding Resolver, and then add fields to the schema corresponding to the main table

type Role {
  name: String
  note: String
}
type User {
  name: String
  gender: Int
  role: Role
}
 
function userResolver () {
  //...
  //return User
}

function roleResolver (userId) {
  //...
  //return Role
}
 

After we wrote roleResolverit, Userwe added a rolefield to it. When the field is requested, graphql will execute it roleResolverto get the data. Let's look at the processing of many-to-many relationships.

Many-to-many relationship

Usually, in Restful API, we will obtain many-to-many related data through a SQL related query, but in graphql, if only the related query is used, it is obviously not full of its characteristics. Let's look at the following example

# schema
type User {
  name: String
  gender: Int
  role: Role
  groups: [Group] #  
  userGroups: [UserGroups] #  
}

type Group {
  id: Int
  name: String
  note: String
  users: [User]
  userGroups: [UserGroups]
}

type UserGroup {
  id: Int
  userId: Int
  groupId: Int
  note: String #  
  user: User
  group: Group
}
 

Schema for the above, it can be seen in the Userincluded groupsand userGroupsthe same, in Groupalso included usersand userGroups. And at UserGroupthe same time contains the Usersum Group, so we can perform the following query

{
  user (id: 1) {
    name
    groups {
      id
      name
      note
      userGroups {
        id
        userId
        groupId
        note
        user {
          name
          groups {
            # ...
          }
        }
      }
    }
  }
}
 

Someone may ask, the above operation loops infinitely. That's right, the loop is indeed infinite. This is not a bug, but the establishment of a connection relationship as I mentioned earlier. For different scenarios, we can query in different ways. For example, when I need to search for a user s user group, I can groupsadd some parameters to it

{
  user (id: 1) {
    name
    groups (name: "admin") {
      id
      name
      note
      userGroups (userId: 1) {
        id
        note
      }
    }
  }
}
 

The above query, if we only want to UserGroupsearch for additional information in the relational table, the above query method can be seen to be unworkable. Then we can query from the other direction

{
  user (id: 1) {
    name
    userGroups (note: " ") {
      id
      userId
      groupId
      note
      group {
        id
        name
        note
      }
    }
  }
}
 

It can be found that after the connected graph of the corresponding relationship is established, we can query from a table to any table that is related to it, and at the same time, we can nest infinitely.

There is no need to worry about the infinite loop problem, because we need to specify the associated field before graphql will execute the corresponding Resolver. If there is an infinite loop, unless our query is also written in an infinite loop, obviously this is impossible.

Basically, we talk about these things in relationship management. If you have a better idea, welcome harassment.

Concluding remarks

This article is my practice and thinking in learning and using graphql. If you have any errors or suggestions, please contact me for correction and discussion. In addition, before practice, you should focus on whether you need to use graphql, because restful api has been able to meet most of the scene requirements, blindly using graphql may bring some unexpected problems.