查看原文
其他

一种灵活的API设计模式:在Spring Boot中支持GraphQL

Vinod Kumar Nair 高可用架构 2019-11-28

导读: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/person https://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;@RequestMapping(“/rest/books”)@RestControllerpublic class BookController { private static Logger logger = LoggerFactory.getLogger(BookController.class);private GraphQLService graphQLService;@Autowired public BookController(GraphQLService graphQLService) { this.graphQLService=graphQLService; }@PostMapping public ResponseEntity<Object> getAllBooks(@RequestBody 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;@Table@Entitypublic class Book {@Id 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:String title:String publisher:String author:[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;@Servicepublic class GraphQLService { private static Logger logger = LoggerFactory.getLogger(GraphQLService.class);private BookRepository bookRepository;private AllBooksDataFetcher allBooksDataFetcher;private BookDataFetcher bookDataFetcher;@Value(“classpath:books.graphql”) Resource resource;private GraphQL graphQL;@Autowired public GraphQLService(BookRepository bookRepository, AllBooksDataFetcher allBooksDataFetcher, BookDataFetcher bookDataFetcher) { this.bookRepository=bookRepository; this.allBooksDataFetcher=allBooksDataFetcher; this.bookDataFetcher=bookDataFetcher; }@PostConstruct private void loadSchema() throws IOException { logger.info(“Entering loadSchema@GraphQLService”); loadDataIntoHSQL();//Get the graphql file File file = resource.getFile();//Parse SchemaF TypeDefinitionRegistry 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;@Componentpublic class AllBooksDataFetcher implements DataFetcher<List<Book>> {private BookRepository bookRepository;@Autowired public AllBooksDataFetcher(BookRepository bookRepository) { this.bookRepository=bookRepository; }@Override 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;@Componentpublic class BookDataFetcher implements DataFetcher<Book> {private BookRepository bookRepository;@Autowired public BookDataFetcher(BookRepository bookRepository){ this.bookRepository = bookRepository; }@Override 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。

{ book(id:”1001"){ title } allBooks{ isn title author publisher publishedDate }}

输出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"){ title author }}

输出2:输出是这个。 我们获得ID为1001的书的标题和作者。

{ “errors”: [], “data”: { “book”: { “title”: “The C Programming Language”, “author”: [ “Brian W. Kernighan (Contributor)”, “Dennis M. Ritchie” ] } }, “extensions”: null}

输入3:让我们查询所有图书的书名,作者,出版日期和出版商详细信息

{ allBooks{ isn title author publisher publishedDate }}

输出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}

在REST API上使用GraphQL的优势就在于响应数据可以根据需求而变化,而不用返回一大堆无关的数据。

源代码:
https://github.com/vinod827/acloudtiger-blog-posts

原文地址:
https://medium.com/@vinod827/spring-boot-app-with-graphql-2dd62a9e5c3e


本文由方圆翻译。转载本文请注明出处,欢迎更多小伙伴加入翻译及投稿文章的行列,详情请戳公众号菜单「联系我们」。


参考阅读:


技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。


高可用架构

改变互联网的构建方式

长按二维码 关注「高可用架构」公众号


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存