Introduction to the

One forAPIQuery language

The official website is always so incisive, simple and rough. (Here again, the official documentation is not very nice.)

As this statement says, GraphQL is a language, a language for API queries. Developed by Facebook as a replacement for the old RESTful architecture, it allows you to describe the data you want declaratively, and it always returns predictable results for every request. It also supports working with multiple languages, whether you’re JS, Java, or Go… (And so on!) , it can give stable support, covering a lot of languages.

Facebook has open-source implementations of the GraphQL standard and its JavaScript version. Later standards were implemented in major programming languages. In addition, the ecosystem around GraphQL not only extends implementations in different languages horizontally, but also has the emergence of class libraries (such as Apollo and Relay) that implement GraphQL on top of it. GraphQL is currently considered a revolutionary API tool because it allows clients to specify the data they want in their requests, rather than being mechanically predefined on the server side like traditional REST.

background

Indeed, the birth of any technology or language is bound to have its difficult historical background.

In today’s world of RESTful architecture, we can almost always figure out how to provide a server-side API without thinking about it while building a front – and back-end project. Of course, we have to be sure that RESTful architectures can still stand the test after so many years, and there must be an indispensable value to them. There are pros and cons, and RESTful also has more or less drawbacks. In a RESTful architecture, retrieving data on demand can be difficult because back-end developers define the data returned on resources at individual urls, rather than front-end developers making data requests.

From the perspective of front and back end interaction, we will always encounter a certain scenario for requesting an interface. The front end needs to pass many parameters in order to obtain or modify specific data. With the continuous iteration of the project, the whole interface request part will become very bloated and difficult to maintain. Not only that, if the presence of server needs to be compatible with multiterminal, an interface that the data returned there might be many outages fields, even possible this page only need a handful of several fields, request interface is returned to the huge amount of data, resulting in a waste of network bandwidth and server processing speed.

In cases where the front end relies on multiple interfaces for page rendering, several related data needs to make multiple requests to satisfy the requirements, which is obviously not a very efficient behavior.

From the perspective of server-side maintenance, for multiple interfaces, regardless of whether there is interface field overlap, we always need to write interface independent documentation for the use of front-end personnel, which is very unfriendly in many scenarios.

advantage

progressive

Adopting GraphQL does not require a complete topdown of the existing technology stack, as if you were planning to move from a monolithic back-end application to a microservice architecture, this would be a good opportunity to introduce the GraphQL API. When your team has multiple microservices, your team can integrate a GraphQL gateway using GraphQL aggregate Schema. You can migrate to GraphQL step by step by bringing all the existing apis together through an API gateway. In this way, you can plug into the GraphQL architecture at a fraction of the cost.

Version management

In traditional RESTful architectures, our interface iterations are often accompanied by multiple API version changes (api.domain.com/v1/, api.domain.com/v2/), and even the coexistence of old and new interfaces. In many cases, front-end personnel call different interfaces without realizing that the interface has been abandoned, and the structure of the new interface has changed, which is a hidden danger for the long-term maintenance of a project.

As for GraphQL, it can be exactly discarded at the field level, and the front-end staff can get a good prompt when using it. You can discard and add various interface fields flexibly, and the caller can be synchronized in real time, which is definitely a friendly way of interaction.

Strongly typed

GraphQL is a strongly typed query Language because it is written through GraphQL Schema Definition Language (SDL). At this point, we can compare ts and JS love and hate, strong verification is undoubtedly significant for code maintainability. GraphQL with some editor plug-ins can not only provide good writing hints, but also error detection of code, avoiding some common syntax errors.

Interface robustness

It would no longer be very unfriendly to spend a certain amount of time battling the back end because the back end changed the fields of the interface without synchronizing the front end. GraphQL can guarantee this because all front-end interfaces have strong type verification. The complete type definition is transparent to the front end, and the error can be detected quickly if the front end query operation does not match the back end interface definition.

Declarative query

As mentioned in the introduction, GraphQL is both an API query language and a declarative query language. Clients can retrieve data declaratively according to business needs. In an interface call, we can define the fields we want, and the server will return the specific field data that the user needs, no more, no less, just right. In this process, the relationship between the client and the server is clear. The client only needs to pay attention to what data it needs, while the server has a clear understanding of its own data structure, and has a definite channel (micro-service, database, third-party API) for the data acquisition of each field, and each plays its own role.

No data overflow

Its declarative queries give clients the ability to fetch on demand, transferring only the required fields for each interaction, without the massive overflow of irrelevant data that occurs in RESTful architectures.

In terms of community ecology, it is maintained by Facebook, and github and Twitter have joined the ranks of GraphQL.

insufficient

Complex query problem

The phenomenon of

That brings us to the N+1 problem, so what is the N+1 problem? Here’s an example:

const allUser = [{id: 1}, {id: 2}, {id: 3}}]
allUser.forEach(item= > {
    queryScore(item.id);
})
Copy the code

As described in the above code, suppose that in the database design, the scores of users and users belong to two tables respectively. First of all, I need to get the data containing all the user IDS and names, and then query the results of users by the user ID. The execution of the above code will lead to the problem that can be solved by looking up the table in one time. Here it takes “N +1” operations to do it, which is obviously unfriendly.

This isn’t just a GraphQL problem, but in some ways it’s more likely to happen than RESTful. This is mainly due to GraphQL layer by layer parsing, as described on the official website:

Each field in the GraphQL query is treated as a supertype function or method that returns a subtype. In fact, that’s exactly how GraphQL works. Each field of each type is supported by a *resolver* function provided by the GraphQL server developers. When a field is executed, the corresponding *resolver* is called to produce the next value.

If the field produces a scalar value, such as a string or number, execution is complete. If a field produces an object, the query continues to execute the parser for that object’s corresponding field until a scalar value is generated. GraphQL queries always end with scalar values.

The solution

For relational databases:

  1. forOne to oneSuch as the one mentioned in the example aboveUserUserScoreWhen the data is fetched from the database, the data is requiredjoinInto a table.
  2. forMany-to-one or many-to-manyYou have to use a relationship called**DataLoader**Tool library. Among them,FacebookNode.jsThe community providesThe realization of the DataLoader.DataLoader The main function of thebatching & caching, can merge multiple database query requests into one, while already loaded data can be directly fromDataLoader In order to handle the problem of complex requests.

The cache

A simple cache, which is a bit more complex to implement in GraphQL than RESTful. In RESTful you access resources through urls, so you can implement caching at the resource level because resources use urls as their identifiers. In GraphQL, it’s complicated because every query is different, even though it operates on the same entity. For example, in one query, you might only ask for an author’s name, but in another query you might also want to know his email address. This requires you to have a more robust mechanism to ensure field-level caching, which is not easy to implement. However, most libraries built on GraphQL provide out-of-the-box caching mechanisms, such as Apollo’s caching capabilities, which are somewhat better for the front end than a RESTful experience.

The reason why GraphQL can’t just add a Header to the server like traditional RESTful architecture (negotiation cache, strong cache) is because RESTful urls are unique and can be easily cached as keys. GraphQL has only one URL. His queries are essentially fetching or modifying data by passing in Schema parameters, so caching capabilities cannot be implemented in the old-fashioned way.

The solution

Here we take Apollo Client as an example, which provides a controlled mechanism for caching policies:

cache-first

Cache first, as the name implies, checks whether the cache is hit before making a request. If so, the data is returned directly. If not, a network request is made to obtain data and update the cache.

cache-and-network

The policy matches the following rules:

When retrieving data, it first checks whether the cache is hit. If so, it returns the data directly. However, different from cache first, regardless of whether the cache is hit or not, it sends a network request to update the cache. The advantage of this approach is that it can ensure the real-time performance of cached data.

network-only

Network only, not cache. This is simpler, that is, for any request, it does not check the cache for a hit, and simply initiates the request to get the latest data.

cache-only

In contrast to network-only, this method only checks if the data is in the cache and throws an error if the data is not in the cache. This strategy is useful when you need to show the user the same data all the time, ignoring changes on the server, or when accessing offline.

no-cache

Also, the ability to name this cache is that all requests go to the network, the cache is not checked, and the data is not cached after the request is received. If you only need the latest data, you can use this scheme.

You can set fetch Policy for the whole application or set it for a single Query, depending on the actual needs of the project. If you do not set a specific policy, Apollo will use cache-first by default.

The core concept

Schema

Defining the structure of the data model, the types of fields, and the relationships between models is at the heart of GraphQL. It’s actually a lot like Typescript, and you’ll find that almost as long as you’re familiar with TS, you’ll be comfortable with typing with Schema.

The ultimate purpose of the type system is to define the shape of an object property, for example if a type explicitly states that the field in the object must be of type Int then you must return an integer for that field.

Scalar type

This is analogous to our typescript primitive types.

  • Int: signed 32-bit integer.
  • Float: signed double precision floating point value.
  • String: UTF‐8 character sequence.
  • Boolean:trueorfalse.
  • IDThe: ID scalar type represents a unique identifier, usually used to retrieve an object or as a key in the cache. The ID type is serialized in the same way as String; However, defining it as an ID means that it does not need to be human-readable.

Most GraphQL service implementations have a way of customizing scalar types. For example, we can define a Date type:

scalar Date
Copy the code

It then depends on how we define serialization, deserialization, and validation in our implementation. For example, you can specify that the Date type should always be serialized as an integer timestamp, and the client should know to require that any Date field be in this format.

Object type and field

The most basic component in building a system using GraphQL is the object type, because the GraphQL architecture is essentially fetching fields on an object.

type ObjectOne {
	name: String
	age: Int
}
Copy the code

This is a basic object type that describes the specific types of the fields in the object, and of course you can nest objects in an object, just like we do with Javascript objects.

Enumerated type

This is actually used in much the same way as in TS, also called enums. An enum type is a special scalar that is limited to a special set of optional values, which are also serialized as strings. This allows you to:

  1. Verify that any parameter of this type is one of the optional values
  2. Communicate with the type system that a field is always one of a finite set of values.

The definition is as follows:

enum Episode {
	ONE
	TWO
	Three
}
Copy the code

That is, if we define an attribute as Episode, its return value must be one of the three values defined in the enumerated type.

The joint type

A union type is equivalent to a collection of types.

Type the Cat {wang: String! } Type Fish{Miao: String! } union Animal = Cat | FishCopy the code

interface

If you’ve been exposed to TS, these concepts are probably a piece of cake for you. The interface is an abstract data type, so only the implementation of the interface is meaningful.

interface Animal {
	name: String
}

type Dog implements Animal {
	name: String
	age: Int
}
Copy the code

The above code indicates that if your type is defined as Dog, then the data you return may contain the name and age attributes (why is it possible, because there is no required identifier added here, more on that later).

List and non-empty

As the name implies, a list is used to describe a set of data rather than a single object or field, and a non-null type is equivalent to specifying that the current field or list must not be empty.

Let’s take a look at what they look like (careful students will notice that non-null types have already appeared in the code above) :

type Character {
  name: String!
  appearsIn: [String]!
  test: [String!]
}
Copy the code

As you can see from the code, some of the field types are followed by **! **, that’s the symbol for a non-empty type, and if it’s added to it, then the field is marked as non-empty, and if it’s added that long after the list it must not be empty meaning it must have at least one value.

We usually use [] to indicate that the field is a list, and then identify the type of each data in the collection in []. In this case, non-null identifiers have different meanings in different places, which will be described below:

test: [String!]
Copy the code

This way of saying that the contents of the list can be empty, but each item in the list can’t be empty, which in this code means it can’t be an empty string.

test: [String]!
Copy the code

If the symbol comes after [], then the list must not be empty, that is, it must have at least one item or more, but it can contain null-valued members.

Input type

Generally we have a requirement that we pass an entire object that contains multiple attributes as query parameters to retrieve data. The input type defines the shape of the object in much the same way as the object type, except that the keyword is input.

input myInput {
	name: String!
	age: Int!
}
Copy the code

It is mainly used when the client calls Mutation to modify data. The specific example will be analyzed in detail later, so I will first understand it here.

Query

From here we are ready to get into the actual operation. Query, in contrast to RESTful, is similar to Router, serving as an entry point for clients to invoke Query data.

Let’s look at the definition in detail:

type Account { name: String age: Int, sex: String, salary(city: String): Int } type Query { name: String age: Int, account(username: String!) : Account accounts: [Account] }Copy the code

We can ignore the parentheses behind the field, which belongs to the Argument section. For details, see the Argument section.

As you can see from the code, we define the Query type, which is used as the entry point to the Query, and we define multiple fields in this object, which are exposed to the client for Query return, and these fields are not limited to scalar types, union types, etc., and not only that, Each of these fields defined in Query should have its own resolver, because they only define their structure here. The return value and logic should be handled by a specific resolver (see the following section resolver for details).

So how does the client query against these fields? In fact, it is also very simple, directly according to his structure of one-to-one query can be implemented as follows:

Query {account(username: "小 生 ") {name sex age} age, name}Copy the code

As shown above, we can query the fields we want, write the names of the fields in the corresponding structure, and the server will return only the fields you define as you want, and nothing else.

One caveat here is that if your target object is not a primitive type, but an object, then you must identify the specific field that you want on that object. That is, it must be exact to a scalar type, otherwise you will get an error, as in the account field above. The type of the account object is Account, and the type of the account object is name, age, sex, and salary fields. When you query the account object, you must specify the value of the field object. If the value is still an object, you must repeat the above steps until you get the scalar type.

Argument

We should be familiar with parameters as well, just like passing parameters to functions. That being said, there is a difference here.

How to pass parameters in GraphQL at the code level

{ salary(city: String!) : Int }Copy the code

In order to get the value of this field, you must pass this parameter. [Resolver] [Resolver] [Resolver] [Resolver] [Resolver] [Resolver] [Resolver]

Query {salary(city: "Shanghai ")}Copy the code

It is also easy to understand, as long as the parameter name is the same as the parameter name you defined on the server side, and then give the specific value.

Resolver

For GraphQL, there is only one entry point, the Root node, from which queries generally start.

Suppose you have a Schema like this:

type UserInfo {
	name: String
	age: Int
}
type Query {
	userinfo(userId: Int): UserInfo
}
Copy the code

The corresponding client query statement is as follows:

query {
	userinfo(userId: 666): {
		name
		age
	}
}
Copy the code

In order to properly respond to the client’s data for these fields, we need to write its resolver for that particular field. For example, the actual data corresponding to the above userinfo is actually returned by a resolver. Usually, a resolver is a corresponding function, and its parameters are:

  • objRepresents the previous object;
  • argsQuery parameters;
  • contextContext, something likeKoathectx;

How to write a resolver for the userinfo field above:

const root = {
	userinfo: function(obj, args, context) {
		const userId = args.userId;
		return {
			name: '李明' + userId,
			age: 18
		}
	}
}
Copy the code

The method of obtaining these data is ignored here, and the data source of these fields can be database or other RPC calls in the actual project. In addition, we should also notice that we can directly fetch the corresponding parameter value from args when processing the field that needs to be passed as a parameter, and then do the logical processing to return it.

This field can be queried only after the resolver is written.

Mutation

Now that you’ve covered Query and Resolver, let’s move on to modifying data.

Before starting on modify the data we need to think about it, if we need to modify a user’s information, we need to put the new information is passed as a parameter in the past, then this parameter type will, it is not a scalar type can represent, so you need to use the input types mentioned above for we preach to participate.

Define input type parameters

First, define the schema

type Account {
    name: String
    age: Int,
    sex: String,
    salary(city: String): Int
}

input AccountInput {
    name: String
    age: Int,
    sex: String
}

type Mutation {
    createAccount(input: AccountInput): Account
}
Copy the code

Query and Mutation are specified and cannot be changed.

The only difference is that the parameter type has changed from String to AccountInput, that is, from a scalar type to an object type, and the way we define the input type has changed from type to input.

Then let’s look at the way the client passes the parameter:

Mutation {createAccount(input: {name: "li" age: 10 sex: "female"}) {name}}Copy the code

In fact, there is no difference between this method and the above method, the only difference is that the type is changed to object method.

In actual combat

The Server side

For the express demonstration, you first need to install several NPM packages:

npm i express express-graphql graphql -S
Copy the code
  • express: Setup Service
  • express-graphql:graphqlRelated middleware
  • graphqlCore package:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

const schema = buildSchema(` type Account { name: String age: Int, sex: String, salary(city: String): Int } type Query { name: String age: Int, account(username: String!) : Account accounts: [Account] } input AccountInput { name: String age: Int, sex: String } type Mutation { createAccount(input: AccountInput): Account } `)

const root = {
    name() {
        return 'strangers'
    },
    age() {
        return 18
    },
    account({ username }) {
        return {
            name: username,
            age: 17.sex: 'male'.salary({ city }) {
                if (city === 'Shanghai') {
                    return 10000
                }
                return 3000}}},createAccount({ input }) {
        db[input.name] = input;
        return input;
    },
    accounts() {
        let arr = []
        for(const key in db) {
            arr.push(db[key])
        }
        return arr
    }
}

let db = {}

const app = express();

app.use(express.static(__dirname + '/public'));

app.use('/graphql', graphqlHTTP({
    schema,
    rootValue:root,
    graphiql: true
}))

app.listen(4000.() = > console.log('listening port: 4000'));
Copy the code

From the example code above, here’s a look at the process logic:

  1. writeschema, defining interface types, etc.
  2. writeresolverIn the code,rootThe individual functions in the object are the fields that need to be exposed to the client to call the queryresolverIs used to handle how to return data
  3. Instantiate aexpressobject
  4. Adding related middleware
  5. Start the service

There are plenty of express tutorials on the web, but I won’t go into detail here. Here are some of the configuration items that graphqlHTTP uses:

  • schemaB: That’s what we mentioned aboveschemaBut I need to use it firstbuildSchemaLet’s deal with stringsschemaStatements (syntactic sugar, so to speak)
  • rootValue: contains all fields that can be accessed by clientsresolverobject
  • graphiql: local debugging tool, used in development environment (great use)

Then let’s see how to debug our service using graphiQL:

You can see that there are two middle sections for the request and response, and there’s a sidebar on the right, which is the document, and if this field is an object, you can go ahead and click on it to see what fields it contains.

The specific query statement is also mentioned above, so I won’t repeat it here, in fact, it is very clear, you need what field, just write the corresponding field name can get the desired response.

The client

Now let’s talk about how to make interface calls on the actual browser side:

<script>
        function queryData() {
            const query = ` query ($username: String!) { account(username: $username) { name sex age } age, name } `

            fetch('/graphql', {
                method: 'POST'.headers: {
                    "Content-Type": 'application/json'."Accept": 'application/json'
                },
                body: JSON.stringify({
                    query,
                    variables: {
                        username: "Footpath"
                    }
                })
            }).then(res= > console.log(res.json()));
        }
    </script>
Copy the code

Again, let’s analyze the process:

  1. Build query parameters, which are the query statements we wrote in the debugging tool above, wrapped in strings, usually using template strings.
  2. Sets the request parameters and request headers
  3. The initiating

To parse the request parameters in more detail, we first need to pass the body a serialized argument that contains:

  • query: Query statement
  • variables: Request parameters used in the statement

$username = “variables”; $username = “variables”; “variables”; It just needs to be marked with a $sign in front.

So the whole front and back end interaction in GraphQL system is almost explained.

conclusion

In general, it’s hard to say whether GraphQL can replace RESTful architecture in the future, but it’s a completely different concept from traditional RESTful architecture. We can experiment with GraphQL in new projects, adapt the architecture in old projects, migrate to GraphQL, or even both. Both architectures have their pros and cons, and we can choose according to our own needs. In short, I believe GraphQL has great development potential, and I hope to make better improvements to the existing shortcomings in the future.

Collation is not easy, kneel beg a praise 😭.