hello,大家好,我是小黑,又和大家见面啦~
今天我们来继续学习 Spring Boot GraphQL 实战,我们使用的框架是 https://github.com/graphql-java-kickstart/graphql-spring-boot
本期,我们将使用 H2 和 Spring Data JPA 来构建数据库和简单的查询,不熟悉的同学可以自行去网上查阅相关资料学习。
完整项目 github 地址:https://github.com/shenjianeng/graphql-spring-boot-example
分页查询
基于偏移量的分页
基于偏移量的分页,即通过 SQL 的 limit 来实现分页。
优点是实现简单,使用成本低。缺点是在数据量过大时,进行大翻页时可能会有性能问题。
先来编写 graphqls
文件:
type PageResult{ items:[Student]! pageNo:Int! pageSize:Int! totalCount:Int!}type Student{ id:ID! name:String!}type Query{ findAll(pageNo:Int!,pageSize:Int!):PageResult!}
对应的 Java Bean 就不在这里赘述了,读者感兴趣的话可以自行查询小黑同学上传在 github 上的源码。
其中,最主要的 StudentGraphQLQueryResolver
源码如下:
@Component@RequiredArgsConstructorpublic class StudentGraphQLQueryResolver implements GraphQLQueryResolver { private final StudentRepository studentRepository; public PageResult<Student> findAll(int pageNo, int pageSize) { Page<Student> page = studentRepository.findAll(PageRequest.of(pageNo - 1, pageSize)); PageResult<Student> pageResult = new PageResult<>(); pageResult.setItems(page.getContent()); pageResult.setPageNo(pageNo); pageResult.setPageSize(page.getSize()); pageResult.setTotalCount((int) page.getTotalElements()); return pageResult; }}
启动应用,测试结果如下图:
基于游标的分页
基于游标的分页,即通过游标来跟踪数据获取的位置。
游标的选取有时候可以非常简单,例如可以将所获得数据的最后一个对象的 ID 作为游标。
GraphQL 游标分页是 Relay 风格式的,更多规范信息可以查阅:https://relay.dev/graphql/connections.htm
Connection 对象
在 Relay 分页查询中,分页结果需要返回 Connection
对象。
先来简单看一下 Connection
的默认实现 graphql.relay.DefaultConnection
的源码:
PageInfo
中保存了和分页相关的一些信息:
编写 graphqls 文件
Relay 式分页中定义了一些规范:
向前分页,在向前分页中,有两个必要参数:
first
和after
first
:从指定游标开始,获取多少个数据after
:指定的游标位置
向后分页,在向后分页中,也有两个必要参数:
last
:指定取游标前的多少个数据before
:与last
搭配使用,用来指定游标位置
type Query{ students(first: Int, after: String): StudentConnection @connection(for: "Student")}
实现分页方法
对应 StudentGraphQLQueryResolver
源码如下:
public Connection<Student> students(int first, String after) { String afterToUsed = StringUtils.defaultIfEmpty(after, "0"); Integer minId = studentRepository.findMinId(); Integer maxId = studentRepository.findMaxId(); // 从 after 游标开始,取 first 个数据 // 这里故意取 first + 1 个数,用来判断是否还有下一页数据 List<Student> students = studentRepository.findByIdGreaterThan(Integer.valueOf(afterToUsed), PageRequest.of(0, first + 1)); List<Edge<Student>> edges = students.stream() .limit(first) .map(student -> new DefaultEdge<>(student, new DefaultConnectionCursor(String.valueOf(student.getId())))) .collect(Collectors.toList()); PageInfo pageInfo = new DefaultPageInfo( new DefaultConnectionCursor(String.valueOf(minId)), new DefaultConnectionCursor(String.valueOf(maxId)), Integer.parseInt(afterToUsed) > minId, students.size() > first); return new DefaultConnection<>(edges, pageInfo);}
更多参考资料:https://www.graphql-java-kickstart.com/tools/relay/
使用 validation 校验参数
在 SpringMVC 中, javax.validation
的一系列注解可以帮我们完成参数校验,那在 GraphQL 中能否也使用 javax.validation
来进行参数合法性校验呢?答案是可行的。
下面,我们就构建一个简单的案例来尝试一下。
type Teacher{ id:ID! name:String! age:Int}type Mutation{ createTeacher(teacherInput:TeacherInput!):Teacher}input TeacherInput{ id:ID! name:String! age:Int!}
@Datapublic class Teacher { private int id; private String name; private int age;}@Datapublic class TeacherInput { @Min(value = 1, message = "id错误") private int id; @Length(min = 2, max = 10, message = "名称过长") private String name; @Range(min = 1, max = 100, message = "年龄不正确") private int age;}@Validated@Componentpublic class TeacherGraphQLMutationResolver implements GraphQLMutationResolver { public Teacher createTeacher(@Valid TeacherInput input) { Teacher teacher = new Teacher(); teacher.setId(input.getId()); teacher.setName(input.getName()); teacher.setAge(input.getAge()); return teacher; }}
可以看到,当客户端输入非法的参数时,服务端参数校验失败,但此时客户端看到的错误信息并不友好。那这个应该如何解决呢?
想想我们在 Spring MVC 中是怎么解决这个问题的?一般,这种情况下,我们会自定义全局异常处理器,然后由这些全局异常处理器来处理这些参数校验失败的异常,同时返回给客户端更友好的提示。
那现在我们是不是也可以这样做呢?我们当前使用的 graphql-spring-boot 框架支不支持全局异常处理呢?
全局异常处理
使用 @ExceptionHandler
Spring MVC 允许我们使用 @ExceptionHandler
来自定义 HTTP 错误响应。
在 graphql-spring-boot 框架中也添加了对该注释的支持,用于以将异常转换为有效的 GraphQLError
对象。
要使用 @ExceptionHandler
注解的方法签名必须满足以下要求:
public GraphQLError singleError(Exception e);public GraphQLError singleError(Exception e, ErrorContext ctx);public Collection<GraphQLError> multipleErrors(Exception e);public Collection<GraphQLError> multipleErrors(Exception e, ErrorContext ctx);
下面,我们就来简单尝试一下。
@Componentpublic class CustomExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) public GraphQLError constraintViolationExceptionHandler(ConstraintViolationException ex, ErrorContext ctx) { return GraphqlErrorBuilder.newError() .message(ex.getMessage()) .locations(ctx.getLocations()) .path(ctx.getPath()) .build(); }}
自定义 GraphQLErrorHandler
第二种处理方式:可以通过实现 graphql.kickstart.execution.error.GraphQLErrorHandler
接口来自定义异常处理器。
需要注意的是,一旦系统中自定义了 GraphQLErrorHandler
组件,那么 @ExceptionHandler
的处理方式就会失效。
@Slf4j@Componentpublic class CustomGraphQLErrorHandler implements GraphQLErrorHandler { @Override public List<GraphQLError> processErrors(List<GraphQLError> errors) { log.info("Handle errors: {}", errors); return Collections.singletonList(new GenericGraphQLError("系统异常,请稍后尝试")); }}
异步 Resolver
异步加载的实现其实也很简单,直接使用 CompletableFuture
作为 Resolver 的返回对象即可。
type Query{ getTeachers:[Teacher]}
@Slf4j@Componentpublic class TeacherGraphQLQueryResolver implements GraphQLQueryResolver { private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); @PreDestroy public void destroy() { executor.shutdown(); } public CompletableFuture<Collection<Teacher>> getTeachers() { log.info("start getTeachers..."); CompletableFuture<Collection<Teacher>> future = CompletableFuture.supplyAsync(() -> { log.info("invoke getTeachers..."); sleep(); Teacher teacher = new Teacher(); teacher.setId(666); teacher.setName("coder小黑"); teacher.setAge(17); return Collections.singletonList(teacher); }, executor); log.info("end getTeachers..."); return future; } private void sleep() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }}
当客户端发起请求时,让我们来一起看一下后台的日志输出,注意看日志输出的先后顺序和执行线程名:
原文转载:http://www.shaoqun.com/a/508613.html
亿恩网:https://www.ikjzd.com/w/1461
淘粉8:https://www.ikjzd.com/w/1725.html
使用GraphQL来实现于基于偏移量和基于游标的分页,同时,实现全局异常处理和异步加载。hello,大家好,我是小黑,又和大家见面啦~今天我们来继续学习SpringBootGraphQL实战,我们使用的框架是https://github.com/graphql-java-kickstart/graphql-spring-boot本期,我们将使用H2和SpringDataJPA来构建数据库和简单的查
李群:李群
vincent:vincent
惊喜放送!跨境知道跨境电商资源工具集合礼包:惊喜放送!跨境知道跨境电商资源工具集合礼包
三亚交通是怎样?:三亚交通是怎样?
Keywordspy:Keywordspy
没有评论:
发表评论