为什么graphql可以替代restful?

Posted 架构师之巅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么graphql可以替代restful?相关的知识,希望对你有一定的参考价值。





引言

为什么graphql可以替代restful?

上图是笔者刚参加工作的时候使用的架构图,当时前后端并未分离,所有代码都刚在一个工程里,如果后端代码改了,前端代码得跟着改,每次改一点,都需要触动全身,非常痛苦;

前后端架构

后来随着公司壮大之后,项目功能也越来越多,改动量也越来越大,实在受不了这种低效率的开发模式了,后来改造成前后端分离的架构,说到前后端分离,它主要分为半分离和完全分离两种类型。

前后端半分离

为什么graphql可以替代restful?


上图是前后端半分离的架构,为什么说是半分离,因为不是所有页面都是单页面应用,在多页面应用的情况下,前端因为没有掌握controller层,前端需要跟后端讨论,我们这个页面是要同步输出呢,还是异步json渲染呢?因此,在这一阶段,只能算半分离。

前后端完全分离

为什么graphql可以替代restful?

上图是完全分离的前后端架构,在这一时期,扩展了前端的范围。认为controller层也属于前端的一部分。在这一时期 前端:负责View和Controller层。后端:只负责Model层,业务处理/数据等。可是前端不懂后台代码呀?controller层如何实现呢?这就是node.js的妙用了,node.js适合运用在高并发、I/O密集、少量业务逻辑的场景。最重要的一点是,前端不用再学一门其他的语言了,对前端来说,上手度大大提高。

restful 简介

说到restful,很多人第一反应是“啊,你说的restful就是那个前后端分离的api吗”,在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

1、https://api.example.com/v1/zoos
2、https://api.example.com/v1/animals
3、https://api.example.com/v1/employees


对于资源的具体操作类型,由HTTP动词表示。常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

1、GET(SELECT):从服务器取出资源(一项或多项)。
2、POST(CREATE):在服务器新建一个资源。
3、PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
4PATCHUPDATE):在服务器更新资源(客户端提供改变的属性)。
5DELETEDELETE):从服务器删除资源。


比如说我要获取一笔orderid为5的订单详情,通常我们都会这样设计;

GET /orders/5

创建一笔新订单,我们一般会这样设计:

POST /orders

restful 缺陷

前端程序员小红希望在 某个restful api接口中新增一个返回值,后端程序员小明由于忙着谈恋爱,没有时间排期增加;这是restful 第一个缺点,前端请求某个接口,希望新增字段的时候,增加了额外的沟通成本;

过了几天,前端程序员小红发现,现有接口无缘无故多返回了几个她不需要的属性,由于web,andorid,ios三个客户端请求的都是同一个接口,因此小明把这三个客户端所有的返回值都返回了。而这些额外的字段,小红并不需要,而且也增加了小红解析返回值的成本。这是restful的第二个缺点,接口的契约只能约定俗成,不能动态按需的请求字段。

graphql 简介

为什么graphql可以替代restful?

GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

这是官网的一段简介,可能听起来有点蒙蔽;简单的说就是之前restful设计的接口,都是由后端控制的,后端接口一旦确定了,就很难更改,因为是很多客户端都使用这个接口,所以无法按照某个特定的客户端去做定制,而且那样成本也是特别高。但是使用graphql就不一样了,数据的主动权掌握在客户端这里,客户端想要什么样的数据,自己直接去请求就好了,无须服务端去做改动。

一个 GraphQL 服务是通过定义类型和类型上的字段来创建的,然后给每个类型上的每个字段提供解析函数。例如,一个 GraphQL 服务告诉我们当前登录用户是 me,这个用户的名称可能像这样:

type Query {
  me: User
}

type User {
  id: ID
  name: String
}


一并的还有每个类型上字段的解析函数:

function Query_me(request{
  return request.auth.user;
}

function User_name(user{
  return user.getName();
}


一旦一个 GraphQL 服务运行起来(通常在 web 服务的一个 URL 上),它就能接收 GraphQL 查询,并验证和执行。接收到的查询首先会被检查确保它只引用了已定义的类型和字段,然后运行指定的解析函数来生成结果。

例如这个查询:

{
  me {
    name
  }
}


会产生这样的JSON结果:

{
  "me": {
    "name""Luke Skywalker"
  }
}


这个时候,前端小红希望在要一个age属性,那就可以这样写:

{
  me {
    name
    age
  }
}


服务端产生的json结果如下:

{
  "me": {
    "name""Luke Skywalker",
    "age":12
  }
}


你看,想要什么字段就在查询语句里增加字段即可,也不用和后端程序员沟通,而后端程序员也可以愉快的谈恋爱了。

Java 操作 graphql

spring-boot 是基于 2.0.0版本,首先加入以下依赖:

<dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-spring-boot-starter</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-tools</artifactId>
            <version>4.3.0</version>
        </dependency>


下面是graphql服务的入口定义:

type Query {
    bookById(id: ID): Book
}

type Book {
    id: ID
    name: String
    pageCount: Int
    author: Author
}

type Author {
    id: ID
    firstName: String
    lastName: String
}


graphql 入口定义了, 但这只是一个描述, 我们需要实现 query中的描述, 在实际开发中,数据的查询应该从db中获取,这里为了方便直接在程序中写死了。

@Component
public class GraphQLDataFetchers {

    private static List<Map<StringString>> books = Arrays.asList(
            ImmutableMap.of("id""book-1",
                    "name""Harry Potter and the Philosopher's Stone",
                    "pageCount""223",
                    "authorId""author-1"),
            ImmutableMap.of("id""book-2",
                    "name""Moby Dick",
                    "pageCount""635",
                    "authorId""author-2"),
            ImmutableMap.of("id""book-3",
                    "name""Interview with the vampire",
                    "pageCount""371",
                    "authorId""author-3")
    );

    private static List<Map<StringString>> authors = Arrays.asList(
            ImmutableMap.of("id""author-1",
                    "firstName""Joanne",
                    "lastName""Rowling"),
            ImmutableMap.of("id""author-2",
                    "firstName""Herman",
                    "lastName""Melville"),
            ImmutableMap.of("id""author-3",
                    "firstName""Anne",
                    "lastName""Rice")
    );

    public DataFetcher getBookByIdDataFetcher() {
        return dataFetchingEnvironment -> {
            String bookId = dataFetchingEnvironment.getArgument("id");
            // TODO 去数据库查数据
            return books
                    .stream()
                    .filter(book -> book.get("id").equals(bookId))
                    .findFirst()
                    .orElse(null);
        };
    }

    public DataFetcher getAuthorDataFetcher() {
        return dataFetchingEnvironment -> {
            Map<StringString> book = dataFetchingEnvironment.getSource();
            String authorId = book.get("authorId");
            // TODO 去数据库查数据
            return authors
                    .stream()
                    .filter(author -> author.get("id").equals(authorId))
                    .findFirst()
                    .orElse(null);
        };
    }
}


定义GraphQL:

@Component
public class GraphQLProvider {

    @Autowired
    GraphQLDataFetchers graphQLDataFetchers;
    private GraphQL graphQL;
    @PostConstruct
    public void init() throws IOException {
        URL url = Resources.getResource("schema.graphqls");
        String sdl = Resources.toString(url, Charsets.UTF_8);
        GraphQLSchema graphQLSchema = buildSchema(sdl);
        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    private GraphQLSchema buildSchema(String sdl) {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
        RuntimeWiring runtimeWiring = buildWiring();
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
    }

    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
                .type(newTypeWiring("Query")
                        .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
                .type(newTypeWiring("Book")
                        .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
                .build();
    }


    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }
}


定义Controller:

@RestController
@RequestMapping("/test")
public class DemoController {
    @Autowired
    private GraphQL graphQL;

    @GetMapping
    public Object find(@RequestParam("query") String query){
        ExecutionResult result = graphQL.execute(query);
        return result.getData();
    }
}


打开postman,使用get请求,请求如下:

http://localhost:8080/test?query={
    bookById(id:"book-1") {
        id
    }
}


返回结果如下:

{
    "bookById": {
        "id""book-1"
    }
}


这个时候,我需要多返回一个name,请求如下:

http://localhost:8080/test?query={
    bookById(id:"book-1") {
        id
        name
    }
}


返回结果如下:

    {
    "bookById": {
        "id""book-1",
        "name""Harry Potter and the Philosopher's Stone"
    }
}


示例很简单,但graphql确实很好地解决了不同接口对查询字段差异性的要求,而不会产生数据冗余,更多功能还待研究。

当然你会说使用graphql会不会产生额外的工作量?当然会增加额外的工作量,前期你需要熟悉graphql的语法,后端需要大量的工作;但是这些额外的工作只是在前期,后期的好处就会体现出来了。

总结

本文介绍了传统的web开发架构的弊端,以及随着业务的增长,如何改造成前后端分离的架构,进而引申出restful 架构,随着restful 架构的缺陷,才有后来的代替者graphql。



  

·END·
 

架构师之巅

时刻关注最前沿的技术

微信号:

jiagoushizhidian




以上是关于为什么graphql可以替代restful?的主要内容,如果未能解决你的问题,请参考以下文章

一文了解 REST 与 GraphQL 的优缺点

Github GraphQL 介绍

为什么说GraphQL可以取代REST API?

GraphQL接口概念及测试ABC

如何在 Java 中使用 GraphQL?

REST, GraphQL, Webhooks & gRPC 如何选型