在我们使用REST API的时候,我们是不是经常遇到这样一些问题?
1、为了适配不同端(手机、Web等)的请求,我们不得不开发多个接口来适配不同端的数据要求。 是不是更有甚者,通过一个接口把能给的全部数据都给前端,让前端自己选择?
2、因为REST API无状态(没有客户端信息存储在请求之间,每一个请求都是分开和独立的)的特性,前端为了完成一个功能不要多次调用不同的接口查询,从而不得不需要一个接口聚合的功能。
现在GraphQL来了!
1、什么是GraphQL
GraphQL是在2012年由Facebook创造,并在2015开源,现在由一个中立的组织GraphQL Foundation管理。几乎所有的语言都有GraphQL规范的实现。
GraphQL的Graph意为用类“图”的形式来描述数据结构。而QL的含义为查询语言,意味着是客户端查询API的方式。
更多了解GraphQL规范可参考:https://spec.graphql.org/
2、Spring for GraphQL与Spring Boot 2.7
对于Java程序员,学习一门新技术最轻松也是最快速的方式是等Spring对某项技术进行集成。
Spring于2022.5.18发布了“Spring for GraphQL v1.0.0”版本,并在2022.5.19日发布Spring Boot 2.7,对“Spring for GraphQL”进行自动配置和指标支持。
Spring for GraphQL基于GraphQL Java(https://github.com/graphql-java/graphql-java),在Spring Boot下使用GraphQL至少需要“spring-boot-starter-graphql”这个starter。
因GraphQL是和传输协议无关的,这就意味着你可以在Spring Web、Webflux、Websocket、RSocket这些传输协议中使用GraphQL。
更多了解Spring for GraphQL可参考:https://docs.spring.io/spring-graphql/docs/current/reference/html/
更多了解Spring Boot 2.7的GraphQL相关信息可参考:https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.graphql
3、Spring Boot 2.7 下的 GraphQL实战
3.1 生成演示项目
添加依赖“Spring for GraphQL”、“Spring Web”,“Websocket”(订阅需要此依赖)。
GraphQL本身和数据库技术是没有关系的,在此处我们为演示方便,使用“Spring Data JPA”和“H2”数据库用来存储和查询数据。
3.2 简单的演示数据实体模型
我们定义两个简单的JPA实体,一个为“书”(Book),一个为“作者”(Author),每本书包含一个作者。
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
@Id
@GeneratedValue
private Long id;
@Column(unique = true)
private String isbn;
private String title;
private Integer pages;
private String authorIdCardNo;
}
上面在Book类里只是引用了authorIdCardNo,并没有映射Author对象,这里其实是DDD(领域驱动设计)推荐的最佳实践,很多人不愿意学JPA(Hibernate)的很多原因都是因为很陡峭的学习曲线,被一些对象之间的关系(一对多、多对多等)搞的糊里糊涂的。
在DDD里,推荐类对象之间不要有硬性的依赖关系(表之间没有外键关系),通过业务ID去互相进行低耦合的关联,业务ID可能是“身份证号码”、“手机号码”或者“订单号”一类的业务唯一标识。
在本例中,Book的业务ID是ISBN号,Author的业务ID是身份证号码。这样Book和Author无论是对象关系还是表关系都没有依赖,保持了松耦合。
在使用DDD设计实体的时候,类对象之间是很少有关系的!
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Author {
@Id
@GeneratedValue
private Long id;
@Column(unique = true)
private String idCardNo;
private String name;
private Integer age;
}
3.3 准备数据访问和演示数据
两个简单的数据访问接口:
public interface BookRepository extends JpaRepository<Book,Long> {
Book findByIsbn(String isbn);
}
public interface BookRepository extends JpaRepository<Book,Long> {
Book findByIsbn(String isbn);
}
让程序启动时自动创建Book和Author的表,关闭时自动删除表:
spring.jpa.hibernate.ddl-auto=create-drop
准备工作做好,我们开始使用Spring Boot 2.7进行GraphQL的实战。
3.2 GraphQL的Schema的基础知识
GraphQL的schema是GraphQL服务器用来描述GraphQL API的。这个schema包含三种类型。
- 对象类型(Object Type)
对象类型对应着我们上面定义的Book和Author实体模型,当然这不是严格对应的,我们可以在GraphQL的对象里定义更多的或者更少的属性。
- 标量类型(Scalar Type)
GraphQL内置了一些标量类型:
1、Int:有符32位整数
2、Float:有符双精度浮点类型
3、String:UTF-8字符序列
4、ID:代表唯一标识,一般用来获取对象
- 根操作类型(Root Operation Type)
根操作类型用来定义如何通过API读写数据。在GraphQL中有三种根操作类型:
1、Query:用来读取数据
2、Mutation:用来创建、更新和删除数据
3、Subscription:类似于Query,用来查询数据;Subscription(订阅)提供一种长时间的操作,当时间改变时数据结果也会有实时变化。
3.3 在Spring Boot中定义GraphQL Schema
Spring Boot会自动在src/main/resources/graphql/** 目录寻找结尾为".graphqls" or ".gqls" 的schema文件,我们可以使用spring.graphql.schema.locations 自定义文件的位置,或使用spring.graphql.schema.file-extensions 定制文件的扩展名。
schema.graphqls :我们也可以按照类型将他们分散在多个文件中
# 对象类型
type Book {
isbn: ID!
title: String!
pages: Int
author: Author!
}
type Author {
idCardNo: ID!
name: String!
age: Int
}
#根操作查询数据
type Query {
allBooks: [Book]!
bookByIsbn(isbn: ID): Book
}
# 根操作创建数据
type Mutation {
createBook(bookInput: BookInput): Book
createAuthor(authorInput: AuthorInput): Author
}
input BookInput { # 定义方法的入参
isbn: ID!
title: String!
pages: Int
authorIdCardNo: String
}
input AuthorInput { # 定义方法的入参
idCardNo: ID!
name: String!
age: Int
}
# 根操作订阅数据
type Subscription {
greetings: String
}
3.4 GraphQL的控制器
我们只需要使用常规的“@Controller”注解就可以将类标注成控制器,通过Spring for GraphQL的“@QueryMapping”、“@MutationMapping”和“@SubscriptionMapping”注解来标注根操作。
和Spring MVC的@RequestMapping类似,Spring for GraphQL也提供了一个“@SchemaMapping”,通过配置不同的参数,从而达到“@QueryMapping”、“@MutationMapping”和“@SubscriptionMapping”一样的效果,但是我们还是会使用特定的注解而不是通用的注解。
- Book的控制器
@Controller
public class BookController {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
public BookController(BookRepository bookRepository, AuthorRepository authorRepository) {
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
}
@QueryMapping
public List<Book> allBooks(){
return bookRepository.findAll();
}
@QueryMapping
public Book bookByIsbn(@Argument String isbn){
return bookRepository.findByIsbn(isbn);
}
@SchemaMapping(typeName = "Book" ,field = "author")
public Author getAuthor(Book book){
return authorRepository.findByIdCardNo(book.getAuthorIdCardNo());
}
@MutationMapping
public Book createBook(@Argument BookInput bookInput){
Book book = new Book();
BeanUtils.copyProperties(bookInput,book);
return bookRepository.save(book);
}
}
特别注意代码
@SchemaMapping(typeName = "Book" ,field = "author")
public Author getAuthor(Book book){
return authorRepository.findByIdCardNo(book.getAuthorIdCardNo());
}
这里是为了schema文件中,类型Book的author属性来获取值的,通过“@SchemaMapping”注解来设置“typeName”和“field”来实现,通过这种方式我们可以把级联多个级别的查询聚合在一起。
新增Book所需的BookInput类很简单:
@Data
public class BookInput {
private String isbn;
private String title;
private Integer pages;
private String authorIdCardNo;
}
- Author控制器
@Controller
public class AuthorController {
private final AuthorRepository authorRepository;
public AuthorController(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@MutationMapping
public Author createAuthor(@Argument AuthorInput authorInput){
Author author = new Author();
BeanUtils.copyProperties(authorInput,author);
return authorRepository.save(author);
}
}
新增Author所用的AuthorInput类也很简单:
@Data
public class AuthorInput {
private String idCardNo;
private String name;
private Integer age;
}
- Greeting控制器
这个控制器是为了演示订阅的,订阅需要有websocket的支持。
@Controller
public class GreetingController {
@SubscriptionMapping
public Flux<String> greetings(){
return Flux.fromStream(Stream.generate(() -> new String("Hello GraphQL " + UUID.randomUUID())))
.delayElements(Duration.ofSeconds(5))
.take(10);
}
}
这里的处理是,每五秒向客户端发送“Hello GraphQL”消息,发送10次。
我们还需指定websocket的端点路径,此时application.properties内容如下:
spring.jpa.hibernate.ddl-auto=create-drop
spring.graphql.websocket.path=/graphql
3.5 使用GraphiQL客户端工具测试
Spring Boot内嵌了一个GraphQL的可视化界面工具GraphiQL来编写和查询数据,需要在application.properties中开启:
spring.jpa.hibernate.ddl-auto=create-drop
spring.graphql.websocket.path=/graphql
spring.graphql.graphiql.enabled=true
启动程序,访问:http://localhost:8080/graphiql
- 首先演示订阅功能:
语句为:
subscription {
greetings
}
你可以看到订阅的数据每五秒钟发生了实时的变化。
- 新增Author语句
mutation{
createAuthor(authorInput: {
idCardNo: "341234567891234567",
name: "汪云飞",
age: 38
}
) {
name
age
}
}
mutation{
createAuthor(authorInput: {
idCardNo: "341234567891234568",
name: "罗伯特C.马丁",
age: 70
}
) {
name
age
}
}
- 新增Book语句
mutation{
createBook(bookInput: {
isbn: "9787121282089",
title: "JavaEE开发的颠覆者:Spring Boot实战",
pages: 524,
authorIdCardNo: "341234567891234567"
}
) {
title
pages
}
}
mutation{
createBook(bookInput: {
isbn: "9787121377921",
title: "从企业级开发到云原生微服务:Spring Boot实战",
pages: 504,
authorIdCardNo: "341234567891234567"
}
) {
title
pages
}
}
mutation{
createBook(bookInput: {
isbn: "9787121347962",
title: "架构整洁之道",
pages: 348,
authorIdCardNo: "341234567891234568"
}
) {
title
pages
}
}
- 查询Book语句
我们可以指定不同的属性,获得不同的查询结果。
语句为:
query{
allBooks {
title
author {
name
}
}
}
结果为:
语句为:
query{
allBooks {
isbn
title
author {
name
age
}
}
}
查询结果为:
通过isbn号查询,语句为:
query{
bookByIsbn(isbn: "9787121377921"){
isbn
title
author {
name
age
}
}
}
结果为:
3.6 使用Postman进行测试
postman支持GraphQL协议,路径填写GraphQL的端点:http://localhost:8080/graphql
在Body中有GraphQL的选项。
3.7 前端的支持
本例显示的是GraphQL的服务端功能,现在GraphQL客户端的支持也很多。
Vue Apollo:https://apollo.vuejs.org/
React/ios/kotlin Apollo Client:https://www.apollographql.com/
Apollo Angular:https://www.the-guild.dev/graphql/apollo-angular
4 结语
如果你对你现在的REST API有些不满意,或者和你配合的前端开发对REST API不满意,在Spring Boot 2.7的加持下,我们可以快速简单的使用GraphQL辅助或替代REST帮助我们进行前后端的接口配合开发。
本文源码地址:https://github.com/wiselyman/graphql-demo
感谢对我的书《从企业级开发到云原生微服务:Spring Boot实战》的支持。
转载请注明出处:今日头条:爱科学的卫斯理
参考资料:
https://tanzu.vmware.com/developer/guides/spring-for-graphql/
https://blog.devgenius.io/graphql-with-java-and-spring-boot-b64d3ce427a2
https://www.baeldung.com/spring-graphql
https://blog.devgenius.io/graphql-with-spring-boot-starter-graphql-7b406998c0b5
https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/
https://www.danvega.dev/blog/2022/05/17/spring-for-graphql/
(此处已添加书籍卡片,请到今日头条客户端查看)