从服务端视角解读GraphQL

GraphQL 由 Facebook 开发,16 年起进入国内,一些企业开始关注并落地。如果用一句话来解释什么是 GraphQL,那就是"增强版 Rest"。

从这个解释以及 GraphQL 诞生的背景来看,GraphQL 是一个为前端而生的技术,有了 GraphQL,前端的边界大大扩展,可以做更多的事情,也不需要和服务端沟通接口协议了。但我作为服务端工程师,想要从服务端视角聊聊 GraphQL。

我们先从 GraphQL 的定义和它对前后端协作方式的改变聊起,然后我再带你分析一个 SpringBoot 集成 GraphQL 的案例。

GraphQL 的定义与协作

GraphQL 的定义

首先我们看看 GraphQL 的定义,GraphQL 是一种针对 Graph(图状数据)进行操作的语言,它是面向前端的,可以暂时理解为增强版的 Rest。从名字上看 GraphQL 和 SQL 很相似,都是某种查询类的语言。图状数据意味着 GraphQL 想要做一些数据的关联查询,而并不是单一维度的数据查询。GraphQL 的兴起离不开前端生产力的发展和壮大,从官网的介绍来看,GraphQL 主打 2 个能力:

  • 获取多个资源只用一个请求

GraphQL 可以把多个请求合并成 1 个请求,可以做到级联查询,这样前端请求起来就很方便了,提升了性能,节省了开发成本。

  • 只获取你需要的字段

GraphQL 可以指定返回你需要的字段,其余的字段不会返回,没有冗余字段。

这两个能力正是前端梦寐以求的能力。但是从服务端视角来看,这两个能力并不完美,首先级联查询真的提升了性能吗?

比如我要查询 5 个同学和这 5 个同学语数外的考试成绩,但是服务端只有获取单个学生单个学科成绩的接口,那么每个同学需要请求 3 次数据库,一共需要请求 15 次数据库,加上获取学生列表的 1 次请求,一共是 16 次。

表面上看,虽然 GraphQL 只需要 1 个请求就可以完成上面的需求,但是背后发生了 16 次调用,显然得不偿失,这就是著名的 N+1 问题,1 次获取学生列表,N 次获取学生成绩。因此决定性能的核心还是在服务端,如果服务端提供一个批量查询的接口就可以解决问题了。可见 GraphQL 的性能还是建立在服务端良好设计的基础之上的。

其次,GraphQL 可以消除前端不需要的字段,这样可以一定程度简化前端的工作量,减少返回数据包的体积,释放网络带宽。当然,这有一定意义,但这并不是很核心的能力。

我们从服务端视角挑战了 GraphQL 的能力,这并不影响前端视角下的 GraphQL 的意义,多个服务聚合成一个服务有些时候是刚需,比如需要在一个页面访问 N 个服务而这个页面需要 H5、IOS、Android、微信小程序、支付宝小程序等各自实现一套,如此一来,服务对接成本如下:

从项目周期和人力投入来看这是绝不能容忍的事情。

GraphQL 的协作

在前后端紧密配合的情况下 GraphQL 完全可以发挥它的优势,这种情况下前端可以更高效、自定义地去获取想要的数据。

具体来说,GraphQL 一定程度上改变了前后端协作模式,常见的协作模式通常有两种:

  • 前端主导:

前端搭建一个中间层 Nodejs 服务,对已有的服务端接口去梳理,整个改造过程对服务端透明。这是目前主流的做法,但是会带来一些问题,包括大流量 Nodejs 的稳定性问题等等。个人认为前端主导并不合适,因为稳定性和领域模型往往对前端透明。

  • 服务端主导:

服务端搭建一个 SpringBoot(以 Java 为例)服务,对已有的服务端接口去梳理,前端根据暴露的新服务来改造。

下面我们来看看服务端主导的情况下,如何接入 GraphQL 呢?

SpringBoot 集成 GraphQL

在服务端主导的情况下,我们可以通过使用 SpringBoot 集成 GraphQL 的方式来接入 GraphQL。GraphQL 集成有很多琐碎的配置,这些网上都能找到,我们今天只聊聊核心的步骤。

GraphQL 最核心的是后缀为.graphqls 的文件,我们下文对集成 GraphQL 的核心步骤的讨论,都将围绕这个文件进行。这个文件里面是一些定义,我们一起来看一下:

typescript 复制代码
type Query {
    user(name: String!): User
    users: [User]
}
type Mutation {
    addUser(name: String!, password: String!): Result
}
type Result {
    respCode: Int!
    msg: String
}
type User {
    id: String!
    name: String!
    password: String!
}

这个文件很简单,我们定义了一个查询类型 Query,这个 Query 下面有 2 个方法,user 方法接受一个 String 类型的 name 并返回对应的 User 类实例,users 方法返回 User 列表。我们还定义了一个操作类型 Mutation,它包含了一个 addUser 方法,接收 String 类型的 name 和 password,返回一个 Result 类对象,我们可以把这些方法名称比作 Spring MVC 中 Controller 的方法名称,这样看上去是不是很直观?

对应的,我们需要创建 java 的 Result 和 User 对象:

typescript 复制代码
@Data
class User {
    private String id;
    private String name;
    private String password;
}

@Data
class Result {
    private Integer respCode;
    private String msg;
}

要实现.graphqls 文件中 Query 类中的方法,需要实现 GraphQLQueryResolve,而要实现.graphqls 文件中 Mutation 类的中的方法,就需要实现 GraphQLMutationResolver。我们来看一下实现它们所需要的代码:

typescript 复制代码
@Component
public class QueryResolver implements GraphQLQueryResolver
{
    @Resource
    private UserService userService;

    public User user(String name) {
        return userService.getUser(name);
    }

    public List<User> users() {
        return userService.listUsers();
    }
}
typescript 复制代码
@Component
public class MutationResolver implements GraphQLMutationResolver
{
    @Resource
    private UserService userService;

    public Result addUser(String name, String password)
   {
        return userService.addUser(name, password);
    }
}

可以看到,我们的方法名称和参数和 graphqls 文件中的配置一致。

最后,可以通过 http://localhost:8080/graphql 来访问 GraphQL 自带的调试工具。比如我们要查询上面的 users 方法,可以使用下面语句,其中 query 是关键字,就像 SQL 中的 select 一样,后面的 test 是这个查询的名称,叫什么都可以。

bash 复制代码
query test{
    users {
      id
      name
    }
}

然后我们访问 users 方法的 id 和 name 字段,返回如下:

json 复制代码
{
    "data": {
        "users": \[
            {
                "id": 1,
                "name": "yafeng"
            },
            {
                "id": 2,
                "name": "yafeng2"
            },
            {
                "id": 3,
                "name": "yafeng3"
            }
        ]
    }
}

前端可以同时往服务端传入多个方法,因而前端的写法可以非常自由。

对 Mutation 类型中的 addUser 方法,我们可以这么调用:

javascript 复制代码
mutation test{
    addUser(name: "yafeng4", password: "123")
    {
      respCode
    }
}

其中 mutation 是关键字, 然后我们调用 addUser 方法并传入参数,并且关注返回中的 respCode 字段,输出结果为:

json 复制代码
{  
    "data": {  
        "addUser": {  
            "respCode": 200  
        }  
    }  
}

是不是很方便?以上是 GraphQL 集成到 SpringBoot 的主要步骤和使用方法,可以看出来还是很简单的。

总结

我们知道 GraphQL 的基础能力包括 2 点,第一点是 GraphQL 可以在获取多个资源时只用一个请求,第二点是 GraphQL 可以只返回你需要的字段。也就是说 GraphQL 可以实现服务的聚合以及冗余字段的处理。

其中在聚合的过程中,GraphQL 有语法可以支持级联操作,但是要注意级联操作的性能取决于后端,如果在没有充分考虑的情况下直接让前端去操作可能会出现性能问题,典型的例子就是我们在前面提到的 N+1 问题。不过,在冗余字段的处理中,前端的工作量得到简化,网络带宽也可以得到释放。

我们也看到 SpringBoot 集成 GraphQL 是很简单的。我们还是推荐服务端去主导这件事情,当然我们也要评估 GraphQL 给公司带来的收益,这取决于公司的发展阶段。GraphQL 给前端带来的改变还是很大的,甚至是颠覆性的,虽然看上去很好但是如果要引入还是需要审慎一些,在充分评估 ROI 的基础上再去建设,相信 GraphQL 会给你的跨团队协作带来意外的收获。

相关推荐
安的列斯凯奇3 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
架构文摘JGWZ3 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC3 小时前
Swift语言的网络编程
开发语言·后端·golang
邓熙榆4 小时前
Haskell语言的正则表达式
开发语言·后端·golang
古蓬莱掌管玉米的神6 小时前
vue3语法watch与watchEffect
前端·javascript
林涧泣6 小时前
【Uniapp-Vue3】uni-icons的安装和使用
前端·vue.js·uni-app
雾恋6 小时前
AI导航工具我开源了利用node爬取了几百条数据
前端·开源·github
专职6 小时前
spring boot中实现手动分页
java·spring boot·后端
拉一次撑死狗7 小时前
Vue基础(2)
前端·javascript·vue.js
Ciderw7 小时前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·