一种灵活的API设计模式:在Spring Boot中支持GraphQL
导读:GraphQL是一种基于api的查询语言,它提供了一种更高效、强大和灵活的数据提供方式。它是由Facebook开发和开源,目前由来自世界各地的大公司和个人维护。本文作者先介绍了GraphQL,随后通过示例详细说明了GraphQL的开发流程是如何使用。
你可能已经听说过GraphQL以及Facebook如何在其移动应用中使用GraphQL。 在此本文中,我将向你展示如何在Spring Boot中组合GraphQL,看看GraphQL到底提供了什么样的功能。
为什么使用GraphQL?
GraphQL是REST API的查询语言。 GraphQL不受任何特定数据库或存储引擎的约束。 你现有的技术架构通常都支持GraphQL。
GraphQL的主要优势:
与REST不同,GraphQL无需在应用程序中创建多个API(应用程序编程接口)endpoint,在REST中,我们公开了多个endpoint以检索此类数据。
https://localhost:8080/personhttps://localhost:8080/person/{id}
使用GraphQL,我们可以按需获取数据。 这与REST不同,在REST实现中,即使只需要一些属性的值,我们也会获取完整的数据响应。 例如,当我们查询REST API时,即使仅需要id和name,我们也会获得如下所示的完整数据。
{“id”: “100”,”name”: “Vijay”,”age”:34"city”: “Faridabad”,”gender”: “Male”}
通过REST API可以将前端(例如移动应用程序)与GraphQL集成在一起,并且响应非常迅速。
本文将介绍如何构建一个Spring Boot应用程序来存储和书查询籍信息。
创建应用
访问Spring Initializr或使用IntelliJ IDEA Ultimate生成具有Web,HSQLDB,Spring Boot 2.1.4等依赖项的Spring Boot应用程序。
生成的POM如下。
<modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version><relativePath/></parent><artifactId>springboot.graphql.app</artifactId><name>springboot-graphql-app</name><description>Demo project for Spring Boot with Graph QL</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.hsqldb</groupId><artifactId>hsqldb</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.graphql-java</groupId><artifactId>graphql-spring-boot-starter</artifactId><version>3.6.0</version></dependency><dependency><groupId>com.graphql-java</groupId><artifactId>graphql-java-tools</artifactId><version>3.2.0</version></dependency></dependencies>
添加EndPoint
让我们从BookController开始,如下所示。
package graphqlapp.controller;import graphqlapp.service.GraphQLService;import graphql.ExecutionResult;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;public class BookController {private static Logger logger = LoggerFactory.getLogger(BookController.class);private GraphQLService graphQLService;public BookController(GraphQLService graphQLService) {this.graphQLService=graphQLService;}public ResponseEntity<Object> getAllBooks( String query){logger.info(“Entering getAllBooks@BookController”);ExecutionResult execute = graphQLService.getGraphQL().execute(query);return new ResponseEntity<>(execute, HttpStatus.OK);}}
添加model
接下来,我们将添加一个model类来代表书。 我们将其命名为Book。 model类的代码如下。
package graphqlapp.model;import javax.persistence.Entity;import javax.persistence.Id;import javax.persistence.Table;public class Book {private String isn;private String title;private String publisher;private String publishedDate;private String[] author;public Book() {}public Book(String isn, String title, String publisher, String publishedDate, String[] author) {this.isn = isn;this.title = title;this.publisher = publisher;this.publishedDate = publishedDate;this.author = author;}public String getIsn() {return isn;}public void setIsn(String isn) {this.isn = isn;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getPublisher() {return publisher;}public void setPublisher(String publisher) {this.publisher = publisher;}public String getPublishedDate() {return publishedDate;}public void setPublishedDate(String publishedDate) {this.publishedDate = publishedDate;}public String[] getAuthor() {return author;}public void setAuthor(String[] author) {this.author = author;}}
创建BookRepository
BookRepository扩展了JpaRepository,如下所示。
package graphqlapp.repository;import graphqlapp.model.Book;import org.springframework.data.jpa.repository.JpaRepository;public interface BookRepository extends JpaRepository<Book, String> {}
添加GraphQL模式(schema)
接下来,我们将在资源文件夹中编写一个GraphQL模式,名为books.graphql。
schema{query:Query}type Query{allBooks: [Book]book(id: String): Book}type Book{isn:Stringtitle:Stringpublisher:Stringauthor:[String]publishedDate:String}
该文件是使用GraphQL的关键。 在这里,我们定义了模式,你可以将其与查询相关联。 我们还定义了查询类型。
在此示例中,我们定义了两种类型:
当用户查询所有书籍(通过使用allBooks)时,应用程序将返回一个Book数组。
当用户通过传递ID查询特定书籍时,应用程序将返回Book对象。
添加GraphQL服务
接下来,我们需要添加GraphQL服务。 让我们将其命名为GraphQLService。
package graphqlapp.service;import graphqlapp.model.Book;import graphqlapp.repository.BookRepository;import graphqlapp.service.datafetcher.AllBooksDataFetcher;import graphqlapp.service.datafetcher.BookDataFetcher;import graphql.GraphQL;import graphql.schema.GraphQLSchema;import graphql.schema.idl.RuntimeWiring;import graphql.schema.idl.SchemaGenerator;import graphql.schema.idl.SchemaParser;import graphql.schema.idl.TypeDefinitionRegistry;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.Resource;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;import java.io.File;import java.io.IOException;import java.util.stream.Stream;public class GraphQLService {private static Logger logger = LoggerFactory.getLogger(GraphQLService.class);private BookRepository bookRepository;private AllBooksDataFetcher allBooksDataFetcher;private BookDataFetcher bookDataFetcher;(“classpath:books.graphql”)Resource resource;private GraphQL graphQL;public GraphQLService(BookRepository bookRepository, AllBooksDataFetcher allBooksDataFetcher,BookDataFetcher bookDataFetcher) {this.bookRepository=bookRepository;this.allBooksDataFetcher=allBooksDataFetcher;this.bookDataFetcher=bookDataFetcher;}private void loadSchema() throws IOException {logger.info(“Entering loadSchema”);loadDataIntoHSQL();//Get the graphql fileFile file = resource.getFile();//Parse SchemaFTypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(file);RuntimeWiring runtimeWiring = buildRuntimeWiring();GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);graphQL = GraphQL.newGraphQL(graphQLSchema).build();}private void loadDataIntoHSQL() {Stream.of(new Book(“1001”, “The C Programming Language”, “PHI Learning”, “1978”,new String[] {“Brian W. Kernighan (Contributor)”,“Dennis M. Ritchie”}),new Book(“1002”,”Your Guide To Scrivener”, “MakeUseOf.com”, “ April 21st 2013”,new String[] {“Nicole Dionisio (Goodreads Author)”}),new Book(“1003”,”Beyond the Inbox: The Power User Guide to Gmail”, “ Kindle Edition”, “November 19th 2012”,new String[] {“Shay Shaked”, “Justin Pot”, “Angela Randall (Goodreads Author)”}),new Book(“1004”,”Scratch 2.0 Programming”, “Smashwords Edition”, “February 5th 2015”,new String[] {“Denis Golikov (Goodreads Author)”}),new Book(“1005”,”Pro Git”, “by Apress (first published 2009)”, “2014”,new String[] {“Scott Chacon”})).forEach(book -> {bookRepository.save(book);});}private RuntimeWiring buildRuntimeWiring() {return RuntimeWiring.newRuntimeWiring().type(“Query”, typeWiring -> typeWiring.dataFetcher(“allBooks”, allBooksDataFetcher).dataFetcher(“book”, bookDataFetcher))build();}public GraphQL getGraphQL(){return graphQL;}}
当Spring Boot应用程序运行时,Spring框架将调用@PostConstruct方法。 @PostConstruct方法中的代码会将书籍信息写入HQL数据库中。
在此服务类的buildRuntimeWiring()方法中,我们将两个数据获取程序进行运行时绑定:allBook和book。 此处定义的名称allBookand book必须与我们已经创建的GraphQL文件中定义的类型匹配。
创建数据访问层
GraphQL模式中的每种类型都有一个对应的数据提取器(data fetcher)。
我们需要为在架构中定义的allBooks和Book类型编写两个单独的数据获取器。
allBooks类型的数据获取程器是这个。
package graphqlapp.service.datafetcher;import graphql.schema.DataFetcher;import graphql.schema.DataFetchingEnvironment;import graphqlapp.model.Book;import graphqlapp.repository.BookRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.List;public class AllBooksDataFetcher implements DataFetcher<List<Book>> {private BookRepository bookRepository;public AllBooksDataFetcher(BookRepository bookRepository) {this.bookRepository=bookRepository;}public List<Book> get(DataFetchingEnvironment dataFetchingEnvironment) {return bookRepository.findAll();}}
Book类型的数据获取器是这个。
package graphqlapp.service.datafetcher;import graphql.schema.DataFetcher;import graphqlapp.model.Book;import graphqlapp.repository.BookRepository;import graphql.schema.DataFetchingEnvironment;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;public class BookDataFetcher implements DataFetcher<Book> {private BookRepository bookRepository;public BookDataFetcher(BookRepository bookRepository){this.bookRepository = bookRepository;}public Book get(DataFetchingEnvironment dataFetchingEnvironment) {String isn = dataFetchingEnvironment.getArgument(“id”);return bookRepository.findById(isn).orElse(null);}}
运行应用
我在端口9002端口上运行此应用程序。 因此,我在application.properties文件中如下。
server.port=9002
这样,我们的Spring Boot GraphQL应用程序就准备好了。 让我们运行我们的Spring Boot应用程序,并使用Postman工具对其进行测试。
注意这里我们只有一个endpoinst,http://localhost:9002/rest/books
让我们使用该单个endpoint查询多个数据集。 为此,请打开Postman并在请求正文中添加以下查询。
输入1:我们要查询一本ID为1001的特定书,并且只需要书名即可。 同时,我们正在查询allBooks,并期望响应将包含is,title,author,publisher和publishedDate。
{:”1001"){title}allBooks{isntitleauthorpublisherpublishedDate}}
输出1:响应如下。
{“errors”: [],“data”: {“book”: {“title”: “The C Programming Language”},“allBooks”: [{“isn”: “1001”,“title”: “The C Programming Language”,“author”: [“Brian W. Kernighan (Contributor)”,“Dennis M. Ritchie”],“publisher”: “PHI Learning”,“publishedDate”: “1978”},{“isn”: “1002”,“title”: “Your Guide To Scrivener”,“author”: [“Nicole Dionisio (Goodreads Author)”],“publisher”: “MakeUseOf.com”,“publishedDate”: “ April 21st 2013”},{“isn”: “1003”,“title”: “Beyond the Inbox: The Power User Guide to Gmail”,“author”: [“Shay Shaked”,“Justin Pot”,“Angela Randall (Goodreads Author)”],“publisher”: “ Kindle Edition”,“publishedDate”: “November 19th 2012”},{“isn”: “1004”,“title”: “Scratch 2.0 Programming”,“author”: [“Denis Golikov (Goodreads Author)”],“publisher”: “Smashwords Edition”,“publishedDate”: “February 5th 2015”},{“isn”: “1005”,“title”: “Pro Git”,“author”: [“Scott Chacon”],“publisher”: “by Apress (first published 2009)”,“publishedDate”: “2014”}]},“extensions”: null}
输入2:让我们再次通过ID查询特定图书的标题和作者。
{book(id:”1001"){titleauthor}}
输出2:输出是这个。 我们获得ID为1001的书的标题和作者。
{“errors”: [],“data”: {“book”: {“title”: “The C Programming Language”,“author”: [“Brian W. Kernighan (Contributor)”,“Dennis M. Ritchie”]}},“extensions”: null}
输入3:让我们查询所有图书的书名,作者,出版日期和出版商详细信息
{allBooks{isntitleauthorpublisherpublishedDate}}
输出3:输出是这个。
{“errors”: [],“data”: {“allBooks”: [{“isn”: “1001”,“title”: “The C Programming Language”,“author”: [“Brian W. Kernighan (Contributor)”,“Dennis M. Ritchie”],“publisher”: “PHI Learning”,“publishedDate”: “1978”},{“isn”: “1002”,“title”: “Your Guide To Scrivener”,“author”: [“Nicole Dionisio (Goodreads Author)”],“publisher”: “MakeUseOf.com”,“publishedDate”: “ April 21st 2013”},{“isn”: “1003”,“title”: “Beyond the Inbox: The Power User Guide to Gmail”,“author”: [“Shay Shaked”,“Justin Pot”,“Angela Randall (Goodreads Author)”],“publisher”: “ Kindle Edition”,“publishedDate”: “November 19th 2012”},{“isn”: “1004”,“title”: “Scratch 2.0 Programming”,“author”: [“Denis Golikov (Goodreads Author)”],“publisher”: “Smashwords Edition”,“publishedDate”: “February 5th 2015”},{“isn”: “1005”,“title”: “Pro Git”,“author”: [“Scott Chacon”],“publisher”: “by Apress (first published 2009)”,“publishedDate”: “2014”}]},“extensions”: null}
参考阅读:
高可用架构
改变互联网的构建方式
