「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
介绍
GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。1
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。2
服务端提供定义好的 schema (数据类型,代表能够提供什么格式的数据),客户端则通过查询语句来主动决定想要的数据格式,数据格式的主动权掌握在了前端,类似数据库中的表和 SQL 的关系,可以查询单表的某个或多个字段,或者多表的联合字段
- GraphQL 能使得前端在开发过程中不在强依赖后端服务的接口和数据格式,前后端的开发是解耦的,能够极大地减少前后端开发人员的沟通成本
- GraphQL 会根据 schema 自动生成 API 文档
实践
本示例基于 Spring Boot 2.2.6 与 Graphql Java 16.1
Spring Boot 2.2 以下版本搭配 GraphQL Java Tools 5.4.x 以上的版本时需要指定 kotlin 的版本
WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x
引入依赖
<!-- com.graphql-java-kickstart 为最新的 groupId, com.graphql-java 已经停止更新 -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>11.0.0</version>
</dependency>
<!-- 可视化调试工具 altair -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>altair-spring-boot-starter</artifactId>
<version>11.0.0</version>
<scope>runtime</scope>
</dependency>
从 maven 仓库可以对比看到 com.graphql-java 的更新时间还是 2018 年
创建 schema 文件
根据不同的业务对象创建不同的 graphqls 文件,并将 Query 和 Mutation 相关 schema 置于 root.graphqls 文件中
# 指定自定义 graphqls 文件路径, 默认为 classpath 下的所以 **/*.graphqls 文件
# 其它配置都是默认开箱即用,无必要无需调整
graphql:
tools:
schemaLocationPattern: graphqls/*.graphqls
JS Graphql 插件支持识别 *.graphql/*.graphqls 文件,可以在声明的 query 和 type 之间的快速跳转,该插件支持 WebStorm、IDEA 等多个相关系列产品
Query(读)
schema
# root.graphqls
schema {
query: Query
}
type Query {
# 需要与 GraphQLQueryResolver 方法签名对应
listAuthors(query: AuthorQuery): [AuthorVO]
getAuthor(id: Int): AuthorVO
}
# author.graphqls
type AuthorVO {
id: Int
name: String
title: String
school: String
}
input AuthorQuery {
name: String
title: String
school: String
}
Code
@Service
public class AuthorQueryResolver implements GraphQLQueryResolver {
private final AuthorRepository repository;
public AuthorQueryResolver(AuthorRepository repository) {
this.repository = repository;
}
public List<AuthorVO> listAuthors(AuthorQuery query) {
List<AuthorDO> list = repository.select(query);
return convertAll(list, AuthorVO.class);
}
public AuthorVO getAuthor(Integer id) {
AuthorDO entity = repository.selectById(id);
return convert(entity, AuthorVO.class);
}
}
Run
# 可以通过 Docs 侧边栏的 ADD QUERY 按钮 快速生成 Query 语句
query{
getAuthor(id: 1){
id
name
title
school
}
}
Mutation(写)
schema
mutation{
saveAuthor(author: {
name: "Tomy"
title: "Teacher"
school: "BU"
})
}
Code
@Service
public class AuthorMutationResolver implements GraphQLMutationResolver {
private final AuthorRepository repository;
public AuthorMutationResolver(AuthorRepository repository) {
this.repository = repository;
}
public Integer saveAuthor(AuthorDTO dto) {
AuthorDO entity = convert(dto, AuthorDO.class);
repository.insert(entity);
return entity.getId();
}
public Boolean updateAuthor(Integer id, AuthorDTO dto) {
AuthorDO entity = convert(dto, AuthorDO.class);
entity.setId(id);
return repository.update(entity);
}
}
分页查询
schema
# root.graphqls
type Query {
# .. other query
pageBook(query: BookQuery): BookConnection
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
# book.graphqls
type BookEdge {
node: BookVO!
cursor: String!
}
type BookConnection {
edges: [BookEdge!]!
pageInfo: PageInfo!
}
Code
// com.jingwu.example.graphql.BookQueryResolver#pageBook
public Connection<BookVO> pageBook(BookQuery query) {
IPage<BookDO> page = repository.selectPage(query);
List<Edge<BookVO>> edges = page.getRecords()
.stream()
.map(book -> new DefaultEdge<>(
convert(book, BookVO.class),
new DefaultConnectionCursor(String.valueOf(book.getId())))
)
.collect(Collectors.toList());
PageInfo pageInfo =
new DefaultPageInfo(
GraphqlUtils.getStartCursorFrom(edges),
GraphqlUtils.getEndCursorFrom(edges),
page.getCurrent() > 1 && page.getCurrent() <= page.getPages(),
page.getPages() > page.getCurrent());
return new DefaultConnection<>(edges, pageInfo);
}
这里用法被当成了普通的分页查询,好像也没啥问题,只要是返回了 Connection 对象就可
没有太理解 Connection 这一种方式,也并没找到使用它原因,官方的说明比较简单;除此仍然可以自定义 Page Response 对象返回分页数据
其它参考:
其它
在 WebMVC 中的拦截器/过滤器、全局异常处理、Validation 校验等等使用方式基本上是一样的
认证过滤器
@Component
public class AuthFilter extends OncePerRequestFilter {
@Autowired
private ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (doAuth(request)) {
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
ExecutionResultImpl build = ExecutionResultImpl.newExecutionResult()
.addError(new GenericGraphQLError("认证失败"))
.data(null)
.build();
response.getWriter().write(objectMapper.writeValueAsString(build.toSpecification()));
return;
}
filterChain.doFilter(request, response);
}
private boolean doAuth(HttpServletRequest request) {
// mock auth result
return RandomUtil.randomBoolean();
}
}
调试工具
Altair 除了 Maven 依赖引入,还有对应的 Chrome 插件,个人感觉比 Maven 依赖稳定好用
除了 Altair,还有以下两种
<!-- to embed GraphiQL tool -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>11.0.0</version>
<scope>runtime</scope>
</dependency>
<!-- to embed Voyager tool -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>voyager-spring-boot-starter</artifactId>
<version>11.0.0</version>
<scope>runtime</scope>
</dependency>