1. Introduction
1.绪论
GraphQL is a query and manipulation language for web APIs. One of the libraries that originated to make working with GraphQL more seamless is SPQR.
GraphQL是一种用于网络API的查询和操作语言。SPQR是为了使GraphQL的工作更加顺利而产生的库之一。
In this tutorial, we’ll learn the basics of GraphQL SPQR and see it in action in a simple Spring Boot project.
在本教程中,我们将学习GraphQL SPQR的基础知识,并在一个简单的Spring Boot项目中看到它的作用。
2. What Is GraphQL SPQR?
2.什么是GraphQL SPQR?
GraphQL is a well-known query language created by Facebook. At its core are schemas – files in which we define custom types and functions.
GraphQL是由Facebook创建的一种著名的查询语言。其核心是模式–我们在其中定义自定义类型和功能的文件。
In the traditional approach, if we wanted to add GraphQL to our project, we would have to follow two steps. First, we’d have to add GraphQL schema files to the project. Secondly, we’d need to write respective Java POJOs representing each type from the schema. This means that we’d be maintaining the same information in two places: in the schema files and in the Java classes. Such an approach is error-prone and requires more effort in maintaining the project.
在传统的方法中,如果我们想把GraphQL添加到我们的项目中,我们将不得不遵循两个步骤。首先,我们必须在项目中添加GraphQL模式文件。其次,我们需要编写各自的Java POJO,代表模式中的每个类型。这意味着我们将在两个地方维护相同的信息:在模式文件中和在Java类中。这种方法容易出错,并且需要在维护项目时付出更多努力。
GraphQL Schema Publisher & Query Resolver, SPQR in short, originated in order to reduce the above problems – it simply generates GraphQL schemas from the annotated Java classes.
GraphQL Schema Publisher & Query Resolver,SPQR的起源是为了减少上述问题–它只是从注释的Java类中生成GraphQL模式。
3. Introducing GraphQL SPQR with Spring Boot
3.用Spring Boot介绍GraphQL SPQR
To see SPQR in action, we’ll set up a simple service. We’re going to use GraphQL Spring Boot Starter and GraphQL SPQR.
为了看到SPQR的运行,我们将建立一个简单的服务。我们将使用GraphQL Spring Boot Starter和GraphQL SPQR。
3.1. Setup
3.1.设置
Let’s start by adding the dependencies for SPQR and Spring Boot to our POM:
让我们先把SPQR和Spring Boot的依赖性添加到我们的POM中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>spqr</artifactId>
<version>0.11.2</version>
</dependency>
3.2. Writing the Model Book Class
3.2.编写模型书类
Now that we’ve added the necessary dependencies, let’s create a simple Book class:
现在我们已经添加了必要的依赖项,让我们创建一个简单的Book类。
public class Book {
private Integer id;
private String author;
private String title;
}
As we can see above, it doesn’t include any SPQR annotations. This can be very useful if we don’t own the source code but would like to benefit from this library.
正如我们在上面看到的,它不包括任何SPQR注释。如果我们不拥有源代码,但想从这个库中受益,这可能非常有用。
3.3. Writing the BookService
3.3.编写BookService
In order to manage the collection of books, let’s create an IBookService interface:
为了管理书籍的收集,让我们创建一个IBookService接口。
public interface IBookService {
Book getBookWithTitle(String title);
List<Book> getAllBooks();
Book addBook(Book book);
Book updateBook(Book book);
boolean deleteBook(Book book);
}
Then, we’ll provide an implementation of our interface:
然后,我们将提供一个我们的接口的实现。
@Service
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@Override
public Book getBookWithTitle(String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@Override
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@Override
public Book addBook(Book book) {
BOOKS_DATA.add(book);
return book;
}
@Override
public Book updateBook(Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@Override
public boolean deleteBook(Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkien", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}
3.4. Exposing the Service with graphql-spqr
3.4.用graphql-spqr暴露服务
The only thing left is creating a resolver, which will expose GraphQL mutations and queries. To do that, we’ll use two important SPQR annotations – @GraphQLMutation and @GraphQLQuery:
剩下的事情就是创建一个解析器,它将暴露GraphQL的突变和查询。为了做到这一点,我们将使用两个重要的SPQR注释–@GraphQLMutation和@GraphQLQuery:。
@Service
public class BookResolver {
@Autowired
IBookService bookService;
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return bookService.getBookWithTitle(title);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
return bookService.addBook(book);
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
return bookService.updateBook(book);
}
@GraphQLMutation(name = "deleteBook")
public void deleteBook(@GraphQLArgument(name = "book") Book book) {
bookService.deleteBook(book);
}
}
If we don’t want to write @GraphQLArgument in every method and are satisfied with GraphQL parameters being named as the input parameters, we can compile the code with the -parameters argument.
如果我们不想在每个方法中写@GraphQLArgument,并且对GraphQL参数被命名为输入参数感到满意,我们可以用-parameters参数编译代码。
3.5. Rest Controller
3.5.休息控制器
Finally, we’ll define a Spring @RestController. In order to expose the service with SPQR, we’ll configure the GraphQLSchema and GraphQL objects:
最后,我们将定义一个Spring @RestController。 为了用SPQR暴露服务,我们将配置GraphQLSchema和GraphQL对象:。
@RestController
public class GraphqlController {
private final GraphQL graphQL;
@Autowired
public GraphqlController(BookResolver bookResolver) {
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withBasePackages("com.baeldung")
.withOperationsFromSingleton(bookResolver)
.generate();
this.graphQL = new GraphQL.Builder(schema)
.build();
}
It’s important to note that we have to register our BookResolver as a singleton.
需要注意的是,我们必须将我们的BookResolver注册为一个单子。
The last task in our journey with SPQR is creating a /graphql endpoint. It’ll serve as a single point of contact with our service and will execute requested queries and mutations:
我们在使用SPQR过程中的最后一项任务是创建一个/graphql端点。它将作为与我们的服务的单一联系点,并将执行所要求的查询和突变。
@PostMapping(value = "/graphql")
public Map<String, Object> execute(@RequestBody Map<String, String> request, HttpServletRequest raw)
throws GraphQLException {
ExecutionResult result = graphQL.execute(request.get("query"));
return result.getData();
}
}
3.6. Result
3.6.结果
We can check the results by inspecting the /graphql endpoint. For example, let’s retrieve all of the Book records by executing the following cURL command:
我们可以通过检查/graphql端点来检查结果。例如,让我们通过执行以下cURL命令来检索所有的Book记录。
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"{getAllBooks {id author title }}"}' \
http://localhost:8080/graphql
3.7. Test
3.7 测试
Once we’re done with the configuration, we can test our project. We’ll use @SpringBootTest to test our new endpoint and validate the responses. Let’s define the JUnit test and autowire the required WebTestClient:
一旦我们完成了配置,我们就可以测试我们的项目。我们将使用@SpringBootTest来测试我们的新端点并验证响应。让我们定义JUnit测试并自动连接所需的WebTestClient。
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = SpqrApp.class)
class SpqrAppIntegrationTest {
private static final String GRAPHQL_PATH = "/graphql";
@Autowired
private WebTestClient webTestClient;
@Test
void whenGetAllBooks_thenValidResponseReturned() {
String getAllBooksQuery = "{getAllBooks{ id title author }}";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(getAllBooksQuery)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.getAllBooks").isNotEmpty();
}
@Test
void whenAddBook_thenValidResponseReturned() {
String addBookMutation = "mutation { addBook(newBook: {id: 123, author: \"J. K. Rowling\", "
+ "title: \"Harry Potter and Philosopher's Stone\"}) { id author title } }";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(addBookMutation)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.addBook.id").isEqualTo("123")
.jsonPath("$.addBook.title").isEqualTo("Harry Potter and Philosopher's Stone")
.jsonPath("$.addBook.author").isEqualTo("J. K. Rowling");
}
private static String toJSON(String query) {
try {
return new JSONObject().put("query", query).toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
4. Using GraphQL SPQR Spring Boot Starter
4.使用GraphQL SPQR Spring Boot Starter
The team working on SPQR has created a Spring Boot starter, which makes using it even easier. Let’s check it out!
从事SPQR工作的团队已经创建了一个Spring Boot启动器,使其使用起来更加容易。让我们来看看!
4.1. Setup
4.1.设置
We’ll start with adding the spqr-spring-boot-starter to our POM:
我们将首先把spqr-spring-boot-starter添加到我们的POM中。
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>0.0.6</version>
</dependency>
4.2. BookService
4.2.书籍服务
Then, we need to add two modifications to our BookService. First of all, it has to be annotated with the @GraphQLApi annotation. In addition, every method we’d like to expose in our API has to have a respective annotation:
然后,我们需要对我们的BookService添加两个修改。首先,它必须用@GraphQLApi注解来标注。此外,我们想在我们的API中公开的每个方法都必须有一个相应的注解。
@Service
@GraphQLApi
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "deleteBook")
public boolean deleteBook(@GraphQLArgument(name = "book") Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkein", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}
As we can see, we basically moved the code from the BookResolver to the BookService. Additionally, we don’t need the GraphqlController class – a /graphql endpoint will be added automatically.
我们可以看到,我们基本上把代码从BookResolver移到了BookService。此外,我们不需要GraphqlController类 – 一个/graphql端点将被自动添加。
5. Summary
5.摘要
GraphQL is an exciting framework and an alternative to traditional RESTful endpoints. While offering a lot of flexibility, it can also add some tedious tasks such as maintaining schema files. SPQR aspires to make working with GraphQL easier and less error-prone.
GraphQL是一个令人兴奋的框架,也是传统RESTful端点的替代品。在提供大量灵活性的同时,它也会增加一些繁琐的任务,如维护模式文件。SPQR希望使GraphQL的工作更容易,更少出错。
In this article, we saw how to add SPQR to the existing POJOs and configure it to serve queries and mutations. Then, we saw a new endpoint in action in GraphQL. Lastly, we verified our application’s behavior using Spring’s testing support.
在这篇文章中,我们看到了如何将SPQR添加到现有的POJO中,并将其配置为服务于查询和突变。然后,我们在GraphQL中看到一个新的端点在运行。最后,我们使用Spring的测试支持验证了我们应用程序的行为。
As always, the sample code used here is available over on GitHub. Additionally, the code for the SPQR Spring Boot starter kit is available over on GitHub.
与往常一样,这里使用的示例代码可在GitHub上找到。此外,SPQR Spring Boot入门套件的代码可在GitHub上找到。