0%

GraphQL的探索上手

一 GraphQL简介

  1. GraphQL是什么

为API而生的专用查询语言

GraphQL是一门为API和运行时而生的查询语言。它可以使用您已有的数据对这些查询进行填充。 GraphQL在您的API中,提供了一个完整的,易于理解的数据描述, 可以给予您的客户端一个权利,可以精确地描述他们所需要的数据,并不拖泥带水。 随着时间的推移,使得API的进化更加容易,并且开启强大的开发者工具

  1. 使用场景

当提起API设计的时候,大家通常会想到SOAP,RESTful等设计方式,从2000年RESTful的理论被提出的时候,在业界引起了很大反响,因为这种设计理念更易于用户的使用,所以便很快的被大家所接受。我们知道REST是一种从服务器公开数据的流行方式。当REST的概念被提及出来时,客户端应用程序对数据的需求相对简单,而开发的速度并没有达到今天的水平。因此REST对于许多应用程序来说是非常适合的。然而在业务越发复杂,客户对系统的扩展性有了更高的要求时,API环境发生了巨大的变化。特别是从下面三个方面在挑战api设计的方式:

  • 移动端用户的爆发式增长需要更高效的数据加载**

Facebook开发GraphQL的最初原因是移动用户的增加、低功耗设备和松散的网络。GraphQL最小化了需要网络传输的数据量,从而极大地改善了在这些条件下运行的应用程序。

  • 各种不同的前端框架和平台

前端框架和平台运行客户端应用程序的异构环境使得我们在构建和维护一个符合所有需求的API变得困难,使用GraphQL每个客户机都可以精确地访问它需要的数据。

  • 在不同前端框架,不同平台下想要加快产品快速开发变的越来越难

持续部署已经成为许多公司的标准,快速的迭代和频繁的产品更新是必不可少的。对于REST api,服务器公开数据的方式常常需要修改,以满足客户端的特定需求和设计更改。这阻碍了快速开发实践和产品迭代。

  1. 接口架构设计

我们以用户账户两个服务作为举例,进行,通过不同的接口设计实现功能.

  • 使用Restful的形式进行获取信息.

    1. 通过/user/{id}获取用户信息
    2. 通过/user/{id}/accounts获取当前用户下所有账户
    3. 通过/account/{id}获取账户余额信息信息

    这期间我们为了获取用户的账户余额需要调用三次接口来完成我们需要的数据.

  • 使用GraphQL的形式进行接口请求

    需求:请求张三这个用户下面的所有账户的余额

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //我们的请求的入参
    query{
    User(id:"d8f5377e-3dea-4830-8e45-bb62f9ca8e8e"){
    name
    age
    identification
    Account(ALL){
    balance
    }
    }
    }

    Post的请求返回结果是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "data":{
    "User":{
    "name":"张三",
    "age":12,
    "identification":"12332112321321321",
    "Account":[
    {"id":"fc9a45ce-dd91-4ed2-8e43-a26412a03edd","balance":12.32},
    {"id":"8d4832be-22f5-40c8-ab37-fc488843d4f1","balance":19999.1}
    ]
    }
    }
    }
    1. 优点

    • 强类型约束

      清晰的定义了每个API支持的操作,包括输入的参数和返回的内容

    • 根据前端需求来获取后端数据

      后端提供什么前端展示什么 —> 前端需要什么,后端提供什么 从思想上的转变,将数据的主动权转移到前端手中.

    • talk is cheap ,show me code.

      • 前端根据页面需求进行定义schema文件,根据schema进行独立开发,有开源的GraphQL模拟后端终端
      • 后端根据schema文件进行解析器编写,这样互相之间分离,有明确的规则文件进行定义接口,排除语言或者文档传达不清的情况, 可以根据定义的规则代码进行简单的静态预发检查,避免不必要的低级错误
    • 版本迭代

      • 可以显示的表明接口过期.
    1. 缺点

      • 会暴露数据库结构(字段名),数据库字段名

      • 嵌套查询,比较费性能

      • 只能用于Post请求,CDN缓存如何做.

      • 只是作为一个树状结构的查询语言,对于command写操作支持不好.

      • 做权限验证比较困难,根据用户的角色来做不同的权限验证,逻辑更复杂.

      • 对于前端减少了沟通,和网络请求次数, 对于后端而言,业务逻辑和数据的处理一点都不少,可能还需要些更多的代码.

      • Facebook仅开源了前端的代码,对于后端的解析和处理逻辑并未进行开源,现在大家使用的都是社区进行的开源,并未接触到有互联网大厂在生产大规模使用此项技术

二. 原理分析

grapql-img-2

客户端 : 不管是手机端还是PC端,请求发送到服务端钱经过 GraphQL Client 转换成客户端Schema, 这里面描述了客户端的对数据的诉求,调用哪个方法, 传递什么样的参数,期望返回值有哪些字段.

服务端 : 服务端拿到这段 Schema 之后,通过事先定义好的服务端 Schema 接收请求参数并执行对应的 resolve 函数提供数据服务。

​ 识别和相应客户端的过程可以看做以下三个过程: 解析 校验 执行

graphql-img-3

  • 解析阶段:为了识别客户端 Schema, graphql-js 定义了一系列的特征标识符:
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
#为了识别客户端 Schema, `graphql-js` 定义了一系列的特征标识符:
export const TokenKind = Object.freeze({
BANG: '!',
DOLLAR: '$',
PAREN_L: '(',
PAREN_R: ')',
SPREAD: '...',
COLON: ':',
EQUALS: '=',
BRACKET_L: '[',
BRACKET_R: ']',
...
});

#定义了 AST 语法树规范
/**
* The set of allowed kind values for AST nodes.
*/
export const Kind = Object.freeze({
// Name
NAME: 'Name',

// Document
DOCUMENT: 'Document',
OPERATION_DEFINITION: 'OperationDefinition',
VARIABLE_DEFINITION: 'VariableDefinition',
VARIABLE: 'Variable',

// Values
INT: 'IntValue',
FLOAT: 'FloatValue',
STRING: 'StringValue',
BOOLEAN: 'BooleanValue',
...
});
  • 校验阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//校验阶段用于验证客户端 Schema 是否按照服务端 Schema 定义好的方式获取数据,比如:获取数据的方法名是否有误,必填项是否有值等等
//如果出现错误,返回结构化的报错信息,还非常人性化的告诉你正确的调用方式是什么
{
"errors":[
{
"message":"Cannot query field "fetchByGen" on type "Query". Did you mean "fetchByGender"?",
"locations":[
{
"line":3,
"column":9
}
]
}
]
}
  • 执行阶段

    执行阶段依赖的输入为:解析阶段的产出物 document ,服务端 Schema;其中 document 准确描述了客户端对数据的述求:请求哪个方法,参数是什么,需要哪些字段;服务端 Schema 描述了提供数据的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"data":{
"User":{
"name":"张三",
"age":12,
"identification":"12332112321321321",
"Account":[
{"id":"fc9a45ce-dd91-4ed2-8e43-a26412a03edd","balance":12.32},
{"id":"8d4832be-22f5-40c8-ab37-fc488843d4f1","balance":19999.1}
]
}
}
}

三. 使用Springboot来搭建GraphQL后端程序

1. 引入开源的GraphQL的相关jar包

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>

注意事项 : 根据SpringBoot的版本不同引入的GraphQL的版本也会不相同

2. 建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE `User` (
`id` varchar(50) NOT NULL AUTO_INCREMENT COMMENT '主键',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`name` varchar(50) DEFAULT NULL COMMENT 'name',
`identification` varchar(50) DEFAULT NULL COMMENT 'identification',
`age` varchar(50) DEFAULT NULL COMMENT 'age',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



CREATE TABLE `Account` (
`id` varchar(50) NOT NULL AUTO_INCREMENT COMMENT '主键',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`balance` varchar(50) DEFAULT NULL COMMENT '余额',
`status` varchar(50) DEFAULT NULL COMMENT '状态',
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3. 重点:建立解析器Resolver

UserResolver.java 用户解析字段

1
2
3
4
5
6
7
8
9
10
@Component
@AllArgsConstructor
public class UserResolver implements GraphQLResolver<Author> {
//数据一种仓库模式,可以使用Spring data jpa来实现
private AccountRepository accountRepository;

public List<Account> getBooks(User user) {
return accountRepository.findByUserId(user.getId());
}

AccountResolver.java 账户信息解析字段

1
2
3
4
5
6
7
8
9
10
@Component
@AllArgsConstructor
public class AccountResolver implements GraphQLResolver<Book> {
//数据一种仓库模式,可以使用Spring data jpa来实现
private UserRepository userRepository;

public User getUser(Account account) {
return authorRepo.findUserById(account.getUserId());
}
}

Mutation.java 用户数据的新增和更新的基本操作

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

@Component
@AllArgsConstructor
public class Mutation implements GraphQLMutationResolver {
private UserRepository userRepository;
private AccountRepository accountRepository;


/**
* 新增用户
*
*/
public User newUser(String name, int age, String identification) {
User user = new User();
author.setName(name);
author.setAge(age);
author.setIdentification(identification);
return userRepository.save(user);
}
/**
* 新增账户
*/
public Account newAccount(String balance, String status, String userId) {
Account account = new Account();
book.setBalance(balance);
book.setStatus(status);
book.setUserId(userId);
return accountRepository.save(account);
}

/**
* 保存/更新账户信息
*/
public Account saveAccount(AccountInput input) {
Account account = new Account();
book.setBalance(balance);
book.setStatus(status);
book.setUserId(userId);
return accountRepository.save(account);
}

/**
* 删除用户下账户信息
*/
public Boolean deleteAccount(String id) {
return accountRepository.deleteById(id);
}

/**
* 更新账户状态
*/
public Account updateAccountStatus(String Status,String id) {
Account account = accountRepository.findAccountById(id);
book.setStatus(Status);
return accountRepository.save(account);
}
}

Query.java查询解析相关代码

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

@Component
@AllArgsConstructor
public class Query implements GraphQLQueryResolver {

private UserRepository userRepo;

private AccountRepo accountRepo;

public User findUserById(Long id) {
return userRepo.findUserById(id);
}

public List<User> findAllUsers() {
return userRepo.findAll();
}

public Long countUsers() {
return userRepo.count();
}

public List<Account> findAllAccounts() {
return accountRepo.findAll();
}

public Long countAccounts() {
return accountRepo.count();
}
}

4. 建立GraphQL文件

在resource下面建立graphq文件夹,在里面包含两个文件root.graphqlsschema.graphqls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//root.graphqls查询入口
type Query {
// User相关
findUserById(id:String!): User

findAllUsers: [User]!

countAuthors: Long!

// Account相关
findAllAccount: [Account]!
countAccounts: Long!
}
String balance, String status, String userId
// 更新/新增
type Mutation {
newUser(name: String!,age: int!,identification: String!) : Author!
newAccount(balance: String!,status: String!, userId: Long!) : Account!
saveAccount(input: AccountInput!) : Account!
deleteAccount(id: String!) : Boolean
updateAccountStatus(status: String!, id:String!) : Account!
}

定义规则前后端都强类型一致的文件,并且要明确注释信息,前后端不用产生歧义

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
// 这是一个组织好的树状结构
type User {
// 用户ID
id: String!
// 创建时间
createdTime: String
// 姓名
name: String
// 年龄
age: int
// 身份证号
identification: String
// 该用户所关联的账户
accounts: [Account]
}

input AccountInput {
balance: String!
status: String!
userId: String!
}

type Account {
id: String!
balance: String!
status: String!
userId: User
}

5 测试

我们将程序跑起来,进行一个简单的测试.GraphQL自带了一个界面化的测试工具并且还有代码提醒功能,这全都归结于我们前期对于root.graphqlsschema.graphqls的定义

http://localhost:8080/graphiql 界面进行测试界面如下

graphql-img-1