GraphQL入门介绍(一)

什么是GraphQL

GraphQL是由FaceBook提出的一种基于API的查询语言(尽管它也支持修改数据)。它能够根据描述按需获取字段数据,不会有任何冗余信息。也能够通过一个请求一次获取多个资源。

GraphQL最早的实现是由FaceBook基于javascript实现,随后几乎被所有主流编程语言支持,已知服务端的实现包含了:C#/.NETClojureElixirErlangGOGroovyJavaJavaScriptPHPPythonScalaRuby,已知客户端的实现包含了:C#/.NETGOJava/AndroidJavaScriptSwift/Objective-C iOSPython。更多信息请参考:http://graphql.cn/code/

GraphQL的优势与劣势

优点:

  1. 可以通过一个请求获取多个或多种资源,避免过多碎片化的请求;Restful强调的是通过资源定位URL,每种类型的资源通过POST、DELETE、PUT、GET来实现增删改查,这样对于一个稍微复杂一点的页面来说,就容易造成碎片化的请求过多。
  2. 字段按需获取,节省冗余数据的加载与传输。

缺点:

  1. 对排序、分页等请求的支持语义上支持的还是比较别扭。
  2. 一个新的语法,看起来类json却又不是json,有一定学习 成本。

Quick Start

本样例采用GraphQL的java实现来演示。

定义POM

`xml pom.xml

com.aqlu

graphql-demo

1.0.0-SNAPSHOTorg.springframework.bootspring-boot-starter-parent1.5.9.RELEASE<disable.checks>true</disable.checks> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

org.springframework.bootspring-boot-starter-webcom.graphql-javagraphql-java7.0com.h2databaseh2org.projectlomboklomboktrue


> `graphql-java`是`GrahpQL`的`Java`实现, 使用`spring-boot`快速构建项目,`lombok`用来精简bean的编写。

### 定义GraphQL的Schema

下面以商品SKU为例来定义我们本次演示的schema,在项目的`resources`目录下新建一个`sku.graphqls`文件:

```graphql resources/sku.graphqls
type Sku {
    id: Int
    name: String
    specs: [Spec]
    stocks: [Stock]
    price: Float
}

type Spec {
    id: Int
    skuId: Int
    name: String
    value: String
}

type Stock {
    id: Int
    skuId: Int
    area: String
    stocks: Int
}

schema {
    query: Query
}

type Query {
    allSkus: [Sku]
    sku(id: Int): Sku
}

如上所示,我们定义了一个Sku类型,其具有id、名称、规格属性、库存信息、价格字段,其中规格属性与库存信息两个字段的类型又分别引用了下面定义的SpecStock两个类型。

接着定义了一个schema,并指明其query使用Query类型来进行组织,Query类型中定义了两个查询方式,allSkus用来查询所有的Sku信息,返回值是Sku的列表;sku用来获取某一个Sku信息,入参需要一个id

编写Java服务端

在设计的场景中,后端的Sku结构与上面定义的Schema结构存在差异。模拟后端采用微服务架构,商品基础信息在一个微服务中、库存在另一个微服务中。

`java com/aqlu/graphql/demo/sku/domain/Sku.java /**

  • SKU实体 */ @Data @Entity public class Sku { @Id private int id;

    private String name;

    @OneToMany(targetEntity = Spec.class, mappedBy = “sku”, cascade = CascadeType.ALL) private Listspecs = new ArrayList<>(); // 规格属性,一对多

    private BigDecimal price;

    public void addSpec(String name, String value) {

     specs.add(new Spec(this, name, value));
    

    } } `

`java com/aqlu/graphql/demo/sku/domain/Spec.java /**

  • 规格属性 */ @Data @Entity @NoArgsConstructor public class Spec { @Id @GeneratedValue private int id;

    private String name;

    private String value;

    @ManyToOne @JoinColumn(name = “sku_id”, nullable = false) private Sku sku;

    public Spec(Sku sku, String name, String value) {

     this.sku = sku;
     this.name = name;
     this.value = value;
    

    } } `

`java com/aqlu/graphql/demo/sku/domain/Stock.java /**

  • 库存实体 */ @Data @NoArgsConstructor @Entity public class Stock { @Id @GeneratedValue private Integer id; private Integer skuId; private String area; private Integer stocks;

    public Stock(Integer skuId, String area, Integer stocks) {

     this.skuId = skuId;
     this.area = area;
     this.stocks = stocks;
    

    } } `

`java com/aqlu/graphql/demo/sku/repository/SkuRepository.java /**

  • SKU信息仓库, JPA实现 */ public interface SkuRepository extends JpaRepository<Sku, Integer> { } `

`java com/aqlu/graphql/demo/sku/repository/StockRepository.java /**

  • 库存信息仓库, JPA实现 */ public interface StockRepository extends JpaRepository<Sku, Integer> { /**
    • 根据sku id获取对应的库存信息 */ ListqueryStocksBySkuId(Integer skuId); } `

`java com/aqlu/graphql/demo/sku/GraphQLService.java /**

  • GrahpQL服务 */ @Service public class GraphQLService { @Value(“classpath:sku.graphqls”) private Resource schemaResource;

    @Autowired private SkuDataFetcher skuFetcher;

    @Autowired private StocksDataFetcher stocksFetcher;

    @Autowired private AllSkusDataFetcher allSkusFetcher;

    private GraphQL graphQL;

    /**

    • GrahpQL dsl 执行入口
    • @param dsl GraphQL dsl查询语言 */ public ExecutionResult query(String dsl) { return graphQL.execute(dsl); }

      @PostConstruct private void loadSchema() throws IOException { // 类型定义注册,加载“sku.graphqls”文件定义的schema TypeDefinitionRegistry registry = new SchemaParser().parse(schemaResource.getFile());

      // 运行时接线 RuntimeWiring runtimeWiring = buildRuntimeWiring();

      // 生成Schema GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(registry, runtimeWiring);

      // 生成graphQL对象 this.graphQL = GraphQL.newGraphQL(graphQLSchema).build(); }

      private RuntimeWiring buildRuntimeWiring() { return RuntimeWiring.newRuntimeWiring()

           .type("Query",
                   runtimeWiring -> runtimeWiring.dataFetcher("allSkus", allSkusFetcher)
                           .dataFetcher("sku", skuFetcher)
           ) // 分别指定Query类型中allSkus字段与sku字段对应的Fetcher
           .type("Sku",
                   runtimeWiring -> runtimeWiring.dataFetcher("stocks", stocksFetcher)
           ) // 指定schemaSku类型的stocks字段对应的Fetcher
           .build();
      

      } } `

`java com/aqlu/graphqls/demo/sku/fetcher/AllSkusDataFetcher.java /**

  • allSkus的数据提取器 */ @Component public class AllSkusDataFetcher implements DataFetcher<List>{

    @Autowired private SkuRepository skuRepository;

    @Override public Listget(DataFetchingEnvironment environment) {

     return skuRepository.findAll();
    

    }

    @PostConstruct public void init(){

     // 添加一些初始化数据
     Sku sku_1 = new Sku();
     sku_1.setId(1);
     sku_1.setName("iPhone X 银色 64G");
     sku_1.setPrice(new BigDecimal("8388.00"));
     sku_1.addSpec("容量", "64G");
     sku_1.addSpec("颜色", "银色");
    
     Sku sku_2 = new Sku();
     sku_2.setId(2);
     sku_2.setName("iPhone X 银色 256G");
     sku_2.setPrice(new BigDecimal("9688.00"));
     sku_2.addSpec("容量", "256G");
     sku_2.addSpec("颜色", "银色");
    
     Sku sku_3 = new Sku();
     sku_3.setId(3);
     sku_3.setName("iPhone X 深空灰色 64G");
     sku_3.setPrice(new BigDecimal("8388.00"));
     sku_3.addSpec("容量", "64G");
     sku_3.addSpec("颜色", "深空灰色");
    
     Sku sku_4 = new Sku();
     sku_4.setId(4);
     sku_4.setName("iPhone X 深空灰色 256G");
     sku_4.setPrice(new BigDecimal("9688.00"));
     sku_4.addSpec("容量", "256G");
     sku_4.addSpec("颜色", "深空灰色");
    
    Sku sku_5 = new Sku();
    sku_5.setId(5);
    sku_5.setName("iPhone 8 深空灰色 256G");
    sku_5.setPrice(new BigDecimal("6888.00"));
    sku_5.addSpec("容量", "256G");
    sku_5.addSpec("颜色", "深空灰色");
    skuRepository.save(Arrays.asList(sku_1, sku_2, sku_3, sku_4, sku_5));
}

}


```java com/aqlu/graphqls/demo/sku/fetcher/SkuDataFetcher.java
/**
 * `sku`的数据提取器
 */
@Component
@Slf4j
public class SkuDataFetcher implements DataFetcher<Sku>{

    @Autowired
    private SkuRepository skuRepository;

    @Override
    public Sku get(DataFetchingEnvironment env) {
        Integer id = env.getArgument("id"); // 获取参数
        try {
            return skuRepository.findOne(id);
        } catch (Exception e) {
            log.error("load sku failed.errMsg:{}", e.getMessage());
            return null;
        }
    }
}

`java com/aqlu/graphqls/demo/sku/fetcher/StocksDataFetcher.java /**

  • stocks的数据提取器 */ @Component public class StocksDataFetcher implements DataFetcher<List> {

    @Autowired private StockRepository stockRepository;

    @Override public Listget(DataFetchingEnvironment env) {

     Sku sku = env.getSource();  // 从上下文环境中获取源对象
     return stockRepository.queryStocksBySkuId(sku.getId());
    

    }

    @PostConstruct private void init() {

     // 添加初始化数据
     Stock stock_1 = new Stock(1, "华东", 10);
     Stock stock_2 = new Stock(1, "华南", 20);
     Stock stock_3 = new Stock(1, "华北", 30);
     Stock stock_4 = new Stock(2, "华东", 40);
     Stock stock_5 = new Stock(2, "华南", 50);
     Stock stock_6 = new Stock(3, "华东", 60);
     Stock stock_7 = new Stock(3, "华北", 70);
     Stock stock_8 = new Stock(4, "华东", 80);
    
     stockRepository.save(Arrays.asList(stock_1, stock_2, stock_3, stock_4, stock_5, stock_6, stock_7, stock_8));
    

    } } `

`java com/aqlu/graphql/demo/sku/SkuController.java /**

  • sku控制器 */ @RestController @RequestMapping(“/sku”) public class SkuController {

    @Autowired private GraphQLService graphQLService;

    @PostMapping(“/query”) public ResponseEntity query(@RequestBody String dsl) {

     ExecutionResult result = graphQLService.query(dsl);
    
     if (result.getErrors().isEmpty()) {
         return ResponseEntity.ok(result);
     } else {
         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
     }
    

    } } `

`java com/aqlu/graphql/demo/GraphqlDemoApplication.java @SpringBootApplication public class GraphqlDemoApplication {

public static void main(String[] args) {
    SpringApplication.run(GraphqlDemoApplication.class, args);
}

}


```properties resources/application.properties
# 开启h2控制台
spring.h2.console.enabled=true

# 打印sql
spring.jpa.show-sql=true

执行查询

启动GraphqlDemoApplication

1. 查询所有sku的id与name信息

$ curl -XPOST -H 'Content-Type:application/json;charset=UTF-8' 'http://localhost:8080/sku/query' -d '{
    allSkus {
        id
        name
    }
}'

返回:

{
    "data": {
        "allSkus": [
            {
                "id": 1,
                "name": "iPhone X 银色 64G"
            },
            {
                "id": 2,
                "name": "iPhone X 银色 256G"
            },
            {
                "id": 3,
                "name": "iPhone X 深空灰色 64G"
            },
            {
                "id": 4,
                "name": "iPhone X 深空灰色 256G"
            },
            {
                "id": 5,
                "name": "iPhone 8 深空灰色 256G"
            }
        ]
    },
    "errors": [],
    "extensions": null
}

2. 查询所有sku的id、name、price信息

$ curl -XPOST -H 'Content-Type:application/json;charset=UTF-8' 'http://localhost:8080/sku/query' -d '{
    allSkus {
        id
        name
        price
    }
}'

返回:

{
    "data": {
        "allSkus": [
            {
                "id": 1,
                "name": "iPhone X 银色 64G",
                "price": 8388
            },
            {
                "id": 2,
                "name": "iPhone X 银色 256G",
                "price": 9688
            },
            {
                "id": 3,
                "name": "iPhone X 深空灰色 64G",
                "price": 8388
            },
            {
                "id": 4,
                "name": "iPhone X 深空灰色 256G",
                "price": 9688
            },
            {
                "id": 5,
                "name": "iPhone 8 深空灰色 256G",
                "price": 6888
            }
        ]
    },
    "errors": [],
    "extensions": null
}

执行上面两个查询时,注意观察后台日志,你会发现每次请求执行的sql为: Hibernate: select sku0_.id as id1_0_, sku0_.name as name2_0_, sku0_.price as price3_0_ from sku sku0_,说明只查询了sku表。

3. 查询所有sku的id、name、price、specs信息

$ curl -XPOST -H 'Content-Type:application/json;charset=UTF-8' 'http://localhost:8080/sku/query' -d '{
    allSkus {
        id
        name
        price
        specs {
            name
            value
        }
    }
}'

查询语句添加了specs字段,同时为spec指定了namevalue字段。 注意:因为specs返回的是Spec类型列表,因此必须要进一步指定需要提取的Spec的字段名。否则会返回语法验证错误:Validation error of type SubSelectionRequired: Sub selection required for type null of field specs

返回:

{
    "data": {
        "allSkus": [
            {
                "id": 1,
                "name": "iPhone X 银色 64G",
                "price": 8388,
                "specs": [
                    {
                        "name": "容量",
                        "value": "64G"
                    },
                    {
                        "name": "颜色",
                        "value": "银色"
                    }
                ]
            },
            {
                "id": 2,
                "name": "iPhone X 银色 256G",
                "price": 9688,
                "specs": [
                    {
                        "name": "容量",
                        "value": "256G"
                    },
                    {
                        "name": "颜色",
                        "value": "银色"
                    }
                ]
            },
            {
                "id": 3,
                "name": "iPhone X 深空灰色 64G",
                "price": 8388,
                "specs": [
                    {
                        "name": "容量",
                        "value": "64G"
                    },
                    {
                        "name": "颜色",
                        "value": "深空灰色"
                    }
                ]
            },
            {
                "id": 4,
                "name": "iPhone X 深空灰色 256G",
                "price": 9688,
                "specs": [
                    {
                        "name": "容量",
                        "value": "256G"
                    },
                    {
                        "name": "颜色",
                        "value": "深空灰色"
                    }
                ]
            },
            {
                "id": 5,
                "name": "iPhone 8 深空灰色 256G",
                "price": 6888,
                "specs": [
                    {
                        "name": "容量",
                        "value": "256G"
                    },
                    {
                        "name": "颜色",
                        "value": "深空灰色"
                    }
                ]
            }
        ]
    },
    "errors": [],
    "extensions": null
}

执行查询时,注意观察后台日志,你会发现此次请求执行的sql为:

Hibernate: select sku0_.id as id1_0_, sku0_.name as name2_0_, sku0_.price as price3_0_ from sku sku0_
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?

说明同时查询了skuspec两张表。

4. 查询指定id的sku信息,包含id、name、price、specs、stocks字段

$ curl -XPOST -H 'Content-Type:application/json;charset=UTF-8' 'http://localhost:8080/sku/query' -d '{
    sku(id: 1) {
        id
        name
        price
        specs {
            name
            value
        }
        stocks {
            area
            stocks
        }
    }
}'

注意查询语句sku指令后面跟了参数id:1

返回:

{
    "data": {
        "sku": {
            "id": 1,
            "name": "iPhone X 银色 64G",
            "price": 8388,
            "specs": [
                {
                    "name": "容量",
                    "value": "64G"
                },
                {
                    "name": "颜色",
                    "value": "银色"
                }
            ],
            "stocks": [
                {
                    "area": "华东",
                    "stocks": 10
                },
                {
                    "area": "华南",
                    "stocks": 20
                },
                {
                    "area": "华北",
                    "stocks": 30
                }
            ]
        }
    },
    "errors": [],
    "extensions": null
}

执行查询时,注意观察后台日志,你会发现此次请求执行的sql为:

Hibernate: select sku0_.id as id1_0_, sku0_.name as name2_0_, sku0_.price as price3_0_ from sku sku0_
Hibernate: select specs0_.sku_id as sku_id4_1_0_, specs0_.id as id1_1_0_, specs0_.id as id1_1_1_, specs0_.name as name2_1_1_, specs0_.sku_id as sku_id4_1_1_, specs0_.value as value3_1_1_ from spec specs0_ where specs0_.sku_id=?
Hibernate: select stock0_.id as id1_2_, stock0_.area as area2_2_, stock0_.sku_id as sku_id3_2_, stock0_.stocks as stocks4_2_ from stock stock0_ where stock0_.sku_id=?

说明同时查询了skuspecstock三张表。

小结

通过上面的Demo,可以发现如果是一个相对简单的需求场景,使用GraphQL反而会增加编写DataFetcher的工作量,因此简单的场景使用Rest风格更合适。

但Graphql带来的灵活度非常高,统一的一个http查询接口,根据不同的dsl即可以得到想要的数据,甚至能为每个字段定义单独的DataFetcher

在大家都在追逐微服务架构的今天,GraphQL的诞生恰逢其实,它能很好的承担起中台的角色,根据不同类型前台页面的展示逻辑,编制各个后台的微服务业务,真正做到按需加载、减少交互次数。

完整示例代码: https://github.com/aqlu/graphql-demo


   转载规则


《GraphQL入门介绍(一)》 Angus_Lu 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Redis有序集合(SortedSet)的POP实现方法 Redis有序集合(SortedSet)的POP实现方法
最近在项目中遇到一个场景需要使用分布式的优先级队列,第一反应就是通过redis的sortedset数据结构来实现。但是阅读其API发现其没有类似List的LPOP与RPOP指令,但是可以根据其提供的ZRANG、ZREVRANGE、ZREM组
2018-05-09 19:50:28
下一篇 
ES日志收集定期清理与备份 ES日志收集定期清理与备份
按天清理索引$ crontab -e ## 每日凌晨1点定时删除30天之前的`logstash-YYYY.MM.DD`索引 0 1 * * * /home/kibana/indexClean.sh es.zyouwei.com logst
2018-01-30 19:20:41
  目录