hello,大叫好,我是小黑,又和大家見面啦~
今天我們來繼續學習 Spring Boot GraphQL 實戰,我們使用的框架是 https://github.com/graphql-java-kickstart/graphql-spring-boot
專案 github 地址:https://github.com/shenjianeng/graphql-spring-boot-example
Query(查詢)
帶引數的查詢
首先,在 classpath 下建立 graphqls 檔案:
type Book{
id:ID!
name:String!
}
type Query{
# 根據 id 查詢 book,引數名為 id,引數型別的 ID 型別,結果返回 book
getBookById(id:ID!):Book
}
建立一個 Spring Bean,此處需要實現 GraphQLQueryResolver
介面,並在該類中自定義一個方法來對映 graphqls 檔案中的查詢。
@Data
public class Book {
private int id;
private String name;
}
@Component
public class BookGraphQLQueryResolver implements GraphQLQueryResolver {
public Book getBookById(int id) {
Book book = new Book();
book.setId(id);
book.setName("這邊書沒有書名");
return book;
}
}
複合欄位查詢
需求:每本書都有作者,在查詢書本資訊時,有時需要返回作者資訊。
# 定義 Author 資料型別結構
type Author{
id:ID!
name:String!
}
type Book{
id:ID!
name:String!
# 增加 author 欄位,資料型別為 Author
author:Author
}
type Query{
# 根據 id 查詢 book,引數名為 id,引數型別的 ID 型別,結果返回 book
getBookById(id:ID!):Book
}
再看一下此時我們的 Java Bean:
@Data
public class Author {
private UUID id;
private String name;
}
@Data
public class Book {
private long id;
private String name;
}
看仔細哦,Book
類中並沒有 author
欄位,Book 中 author 資訊將由 graphql.kickstart.tools.GraphQLResolver
來提供。
@Slf4j
@Component
public class BookGraphQLResolver implements GraphQLResolver<Book> {
public Author author(Book book) {
log.info("book id :{} query author info", book.getId());
Author author = new Author();
author.setId(UUID.randomUUID());
author.setName(String.format("我是[%s]的作者", book.getName()));
return author;
}
}
ok,讓我們啟動服務,訪問 http://localhost:8080/graphiql
而當客戶端不需要 author 資訊時,服務端就不會執行 BookGraphQLResolver#author
,真正做到了使得客戶端能夠準確地獲得它需要的資料,而且沒有任何冗餘。
(ps:如果你是服務端開發,你會怎麼實現呢?是給客戶端提供一個介面返回 book 和 author 資訊,還是給客戶端提供兩個不同的介面呢?)
Mutation(變更)
在 graphqls 檔案中,使用 Query 來定義查詢介面,使用 Mutation 可以定義變更資料的操作。
type Mutation{
createBook(id:ID!,name:String!):Book
}
上述 graphqls 檔案中定義了一個 createBook
的方法,引數列表為 id
和 name
,方法返回建立的 Book
物件。
與之對應的 Java 程式碼如下:
@Component
public class BookGraphQLMutationResolver implements GraphQLMutationResolver {
public Book createBook(int id, String name) {
Book book = new Book();
book.setId(id);
book.setName(name);
return book;
}
}
BookGraphQLMutationResolver
實現了 graphql.kickstart.tools.GraphQLMutationResolver
介面,表明當前類中的方法用來對映 graphqls 檔案中的 Mutation。
Input Types
當 Mutation 中請求引數特別多時,我們可以使用 Input Types 來優化程式碼。
type Mutation{
createBook(id:ID!,name:String!):Book
create(bookInput:BookInput!):Book
}
input BookInput{
id:ID!
name:String!
}
同理,我們也需求在 BookGraphQLMutationResolver
中新增對應的方法來對映。
@Component
public class BookGraphQLMutationResolver implements GraphQLMutationResolver {
// ...省略其他程式碼
public Book create(BookInput input) {
Book book = new Book();
book.setId(input.getId());
book.setName(input.getName());
return book;
}
}
客戶端請求程式碼如下:
自定義標量型別
在 GraphQL 中自帶一些預設標量型別:
-
Int
:有符號 32 位整數 -
Float
:有符號雙精度浮點值 -
String
:UTF‐8 字元序列 -
Boolean
:true
或者false
-
ID
:ID 標量型別表示一個唯一識別符號,通常用以重新獲取物件或者作為快取中的鍵。ID 型別使用和 String 一樣的方式序列化
使用 graphql-java-extended-scalars 庫
在 Java 這個生態中,我們可以引入下面這個庫來幫助我們很方便的進行擴充套件:
https://github.com/graphql-java/graphql-java-extended-scalars
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
<version>15.0.0</version>
</dependency>
graphql-java-extended-scalars
中具體擴充套件了哪些標量型別,我們都可以在 graphql.scalars.ExtendedScalars
類中找到。
(ps:一個小技巧,s 結尾的類一般都是工具類)
如何使用呢?
- 向 Spring 容器中註冊自定義標量
- 在 graphqls 檔案中宣告要使用的自定義標量
- 直接使用即可
相關示例程式碼如下:
@Configuration
public class CustomScalarTypeConfig {
@Bean
public GraphQLScalarType graphQLLong() {
return ExtendedScalars.GraphQLLong;
}
}
scalar Long
type Book{
id:ID!
name:String!
# 增加 author 欄位,資料型別為 Author
author:Author
totalPageSize:Long
}
使用 GraphQLScalarType 自定義標量型別
我們可以參考 graphql.scalars.java.JavaPrimitives#GraphQLLong
的實現來自定標量型別。
@Bean
public GraphQLScalarType graphQLDate() {
return GraphQLScalarType
.newScalar()
.name("Date")
.description("Date 型別")
.coercing(new Coercing<Date, String>() {
@Override
public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
return new SimpleDateFormat(DATE_FORMAT_PATTERN_DEFAULT).format((Date) dataFetcherResult);
}
@Override
public Date parseValue(Object input) throws CoercingParseValueException {
if (input instanceof String) {
try {
return new SimpleDateFormat(DATE_FORMAT_PATTERN_DEFAULT).parse((String) input);
} catch (ParseException e) {
throw new CoercingParseValueException(e);
}
}
throw new CoercingParseValueException(
"Expected a 'String' but was '" + Kit.typeName(input) + "'."
);
}
@Override
public Date parseLiteral(Object input) throws CoercingParseLiteralException {
if (!(input instanceof StringValue)) {
throw new CoercingParseLiteralException(
"Expected AST type 'StringValue' but was '" + typeName(input) + "'."
);
}
try {
return new SimpleDateFormat(DATE_FORMAT_PATTERN_DEFAULT).parse(((StringValue) input).getValue());
} catch (ParseException e) {
throw new CoercingParseValueException(e);
}
}
})
.build();
}
DataFetcherResult
在 Resolver 中,我們可以使用 graphql.execution.DataFetcherResult
來包裝返回的結果,示例程式碼如下:
@Component
public class BookGraphQLQueryResolver implements GraphQLQueryResolver {
public DataFetcherResult<Book> getBookById(int id) {
if (id <= 0) {
return DataFetcherResult
.<Book>newResult()
.error(new GenericGraphQLError("id 不能為負數"))
.build();
}
Book book = new Book();
book.setId(id);
book.setName("這邊書沒有書名");
return DataFetcherResult
.<Book>newResult()
.data(book)
.build();
}
}
下期預告
下期我們將使用 graphQL 來實現分頁,並介紹一些高階特性,例如:非同步載入、全域性異常處理等。感謝大家的關注和閱讀~~
更多學習參考資料:
https://www.graphql-java-kickstart.com/tools/schema-definition/#resolvers-and-data-classes