GraphQL 已经出现很久了,最近我在看 Spring 文档时,看到了相关的介绍(我记得以前是没的,不知道什么时候的新特性),就试了试,先来一段AI的介绍。如果你仅想看怎么实现,跳过下面这段
来着 Codeium AI 的介绍
GraphQL是一种用于API开发的查询语言和运行时环境。它由Facebook开发并于2015年开源。GraphQL的主要目标是提供一种更高效、灵活和易于使用的方式来获取和操作数据。与传统的RESTful API相比,GraphQL允许客户端精确地指定需要的数据,并减少了不必要的网络传输和数据处理。
GraphQL的核心思想是客户端通过发送查询请求来获取所需的数据,而服务器则返回与请求匹配的结果。客户端可以精确地指定需要的字段和关联关系,而不需要在每个请求中获取整个对象的所有属性。这样可以避免过度获取不需要的数据,并减少网络传输和服务器负载。
GraphQL还提供了强大的类型系统,允许定义字段的类型和验证规则。这样可以在编译时检测错误,并提供更好的开发体验和文档。
总而言之,GraphQL是一种现代化的API查询语言和运行时环境,它提供了更高效、灵活和易于使用的方式来获取和操作数据。它已经被广泛采用,并在许多大型应用程序中取得了成功。
创建 GraphQL schema
以 我的一个项目 为例,为省市区的接口创建GraphQL
首先,在 resources/graphql/ 下,创建 address.graphqls (后缀也可以是.gqls)
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
   | type Query {     provinces: [Province!]!     province(code: ID!): Province     city(code: ID!): City     area(code: ID!): Area }
  type Province {     code: ID!     name: String!     cities: [City!]! }
  type City {     code: ID!     name: String!     provinceCode: Int!     areas: [Area!]! }
  type Area {     code: ID!     name: String!     cityCode: Int!     provinceCode: Int! }
 
  | 
 
GraphQL 有两个入口,一个 Query 用于查询数据, 一个是 Mutation 用于更新对象,基础类型叫 Scalars,默认有以下六种 ID,String,Int,Float,Boolean,List。除了ID,其他都是很熟悉的,ID在 GraphQL 中等价于 String,不同的是ID是唯一的,所以可以作为Cache的依据
配置 RuntimeWiringConfigurer
Spring GraphQL 会依据配置好的 RuntimeWiringConfigurer 获取数据,使用dataFetcher获取type中对应field内容,我们可以通过environment获取GraphQL的上下文内容,比如getArgument获取参数值
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
   | @Configuration public class AddressGraphQlConfiguration {
      @Bean     RuntimeWiringConfigurer customWiringConfigurer(AddressService addressService) {         return wiringBuilder -> wiringBuilder                 .type("Query", builder -> builder                         .dataFetcher("provinces", environment -> {                             return addressService.getProvinces();                         })                         .dataFetcher("province", environment -> {                             String code = environment.getArgument("code");                             return addressService.getProvince(Integer.parseInt(code)).orElseThrow();                         })                         .dataFetcher("city", environment -> {                             String code = environment.getArgument("code");                             return addressService.getCity(Integer.parseInt(code)).orElseThrow();                         })                         .dataFetcher("area", environment -> {                             String code = environment.getArgument("code");                             return addressService.getArea(Integer.parseInt(code)).orElseThrow();                         }))                 .type("Province", builder -> builder                         .dataFetcher("cities", environment -> {                             Province source = environment.getSource();                             return addressService.getCitiesByPCode(source.code());                         }))                 .type("City", builder -> builder                         .dataFetcher("areas", environment -> {                             City source = environment.getSource();                             return addressService.getAreasByPCode(source.code());                         }));     }
  }
 
  | 
 
Spring 也支持注解形式配置,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | @Controller public class GreetingController {
          @QueryMapping          public String hello() {              return "Hello, world!";         }
          @SchemaMapping(typeName="Greeting", field="author")         public Author getAuthor(Greeting greeting) {                      }      }
 
  | 
 
Spring 会使用内置的AnnotatedControllerConfigurer,配置所有注解的到RuntimeWiring中,所以上面等价于
1 2 3 4 5 6 7 8 9 10 11 12 13
   | @Bean RuntimeWiringConfigurer customWiringConfigurer() {     return wiringBuilder -> wiringBuilder             .type("Query", builder -> builder                     .dataFetcher("hello", environment -> {                         return "Hello, world!";                     }))             .type("Greeting", builder -> builder                     .dataFetcher("author", environment -> {                         Greeting source = environment.getSource();                                              })); }
 
  | 
 
@QueryMapping 等价于 @SchemaMapping(typeName="Query"),field如果没有定义,会获取方法名,typeName同理,会获取Controller的前面的名字
总结
上面的代码在我的项目Api Core中,需要参考的可以去看看,我对于GraphQL的评价是,它确实能实现,所要即所得,不会返回多于的数据,但是,在一定程度上隐藏了对象之间的复杂性,这对应编程人员来说要求会更高
举个例子,上面的服务如果获取全部的省市区数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | {     provinces {         code         name         cities {             code             name             areas {                 code                 name             }         }     } }
 
 
  | 
 
这会对每个Province实例调用Province中定义cities,在City也一样,所以,一旦在type中直接使用了调用数据库的查询,那么这就等于for循环里不断的调用,所以需要使用 DataLoader 或者 @BatchMapping (是不是相对于RESTFul里显性的填充,在GraphQL中更难被察觉)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | @Controller public class BookController {
      public BookController(BatchLoaderRegistry registry) {         registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {                      });     }
      @SchemaMapping     public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {         return loader.load(book.getAuthorId());     }
           @BatchMapping     public Mono<Map<Book, Author>> author(List<Book> books) {              }
  }
 
  | 
 
当然我的那个项目,省市区数据都在内存中,所以可以任性的用在type中直接获取,所以相对于RESTFul来说,各有优劣而已