什么是GraphQL
GraphQL是由FaceBook提出的一种基于API的查询语言(尽管它也支持修改数据)。它能够根据描述按需获取字段数据,不会有任何冗余信息。也能够通过一个请求一次获取多个资源。
GraphQL最早的实现是由FaceBook基于javascript实现,随后几乎被所有主流编程语言支持,已知服务端的实现包含了:C#/.NET
、Clojure
、Elixir
、Erlang
、GO
、Groovy
、Java
、JavaScript
、PHP
、Python
、Scala
、Ruby
,已知客户端的实现包含了:C#/.NET
、GO
、Java/Android
、JavaScript
、Swift/Objective-C iOS
、Python
。更多信息请参考:http://graphql.cn/code/
GraphQL的优势与劣势
优点:
- 可以通过一个请求获取多个或多种资源,避免过多碎片化的请求;Restful强调的是通过资源定位URL,每种类型的资源通过POST、DELETE、PUT、GET来实现增删改查,这样对于一个稍微复杂一点的页面来说,就容易造成碎片化的请求过多。
- 字段按需获取,节省冗余数据的加载与传输。
缺点:
- 对排序、分页等请求的支持语义上支持的还是比较别扭。
- 一个新的语法,看起来类json却又不是json,有一定学习 成本。
Quick Start
本样例采用GraphQL的java
实现来演示。
定义POM
`
xml pom.xml
> `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
、名称、规格属性、库存信息、价格字段,其中规格属性与库存信息两个字段的类型又分别引用了下面定义的Spec
与Stock
两个类型。
接着定义了一个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 List
specs = 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获取对应的库存信息 */ List
queryStocksBySkuId(Integer skuId); } `
- 根据sku id获取对应的库存信息 */ List
`
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 List
get(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 List
get(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
指定了name
与value
字段。 注意:因为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=?
说明同时查询了
sku
、spec
两张表。
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=?
说明同时查询了
sku
、spec
、stock
三张表。
小结
通过上面的Demo,可以发现如果是一个相对简单的需求场景,使用GraphQL反而会增加编写DataFetcher
的工作量,因此简单的场景使用Rest风格更合适。
但Graphql带来的灵活度非常高,统一的一个http查询接口,根据不同的dsl即可以得到想要的数据,甚至能为每个字段定义单独的DataFetcher
。
在大家都在追逐微服务架构的今天,GraphQL
的诞生恰逢其实,它能很好的承担起中台
的角色,根据不同类型前台
页面的展示逻辑,编制各个后台
的微服务业务,真正做到按需加载、减少交互次数。