2023年6月14日 星期三

Java GraphQL 實作練習 - 不使用 SpringBoot,只使用一般 Spring MVC

這篇文要來紀錄一下 Java 的 GraphQL 純 Spring MVC 實作練習,
因為網路上的許多範例都是使用 SpringBoot 來實作,
所以在這裡紀錄一下使用純 Spring MVC 、不使用 SpringBoot 的方法。

在這裡想要實作的情境是:
我們有 Author、和 Book 兩種資料,
Author 和 Book 都有 id、name 屬性,
Author 還有 bookIdList 屬性,是一個 Array,紀錄著此 Author 撰寫的 Book 的 id。


我們要實作兩個 query 和一個 mutation,分別是:
Author  getAuthorById(id) :
用 Author id 查 Author。
回傳的 bookList 可以給一個 prefix 參數,它可以為回傳的 book name 前面加上 prefix
(這只是用來練習如何在 Java GraphQL server 端取得 Query 的參數)。

Book  getBookById(id) :
用 Book id 查 Book,

Author updateAuthorName(id, name):
修改特定 id 的 Author 的 name。

GraphQL 的 Schema 設計如下,其中 Date 是我們自訂的 type,之後會建立 GraphQLScalarType 類別來解析它:

scalar Date

type Book {
	id: ID!
	name: String
    publishDate: Date	
}

type Author {
	id: ID!
	name: String
	bookList(prefix: String): [Book]
}

type Query {
  getAuthorById(id: ID!): Author
  getBookById(id: ID!): Book
}

type Mutation {
    updateAuthorName(id: ID!, name: String!): Author
}

Query Request 的例子如下:

query {
  getAuthorById(id: 3) {
    id
    name
    bookList(prefix: "test") {
        id
        name
        publishDate
    }
  }
  
  getBookById(id: 33) {
    id
    name
    publishDate
  }
}

Mutatio Request 的例子如下:

mutation {
    updateAuthorName(id: 3, name: "Author 333") {
        id
        name
    }
}

以下開始實作,這裡使用 Opoen JDK 11,
先建立一個 archetype 為 maven-archetype-webapp 的 Java Dynamic Web Application Maven 專案,可以參考這裡
首先先來看一下專案的結構設計,如下圖所示:

接著開始說明,

因為我們要使用 Spring MVC 和 GraphQL 的 library,所以在 pom.xml 加上以下 dependency:

pom.xml (部份內容):

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-webmvc</artifactId>
	    <version>5.1.3.RELEASE</version>
	</dependency>

	<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-context-support</artifactId>
	    <version>5.1.3.RELEASE</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>3.0.1</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
	<dependency>
	    <groupId>org.aspectj</groupId>
	    <artifactId>aspectjweaver</artifactId>
	    <version>1.9.6</version>
	</dependency>
    
    <!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java -->
	<dependency>
	    <groupId>com.graphql-java</groupId>
	    <artifactId>graphql-java</artifactId>
	    <version>20.2</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/com.graphql-java-kickstart/graphql-java-tools -->
	<dependency>
	    <groupId>com.graphql-java-kickstart</groupId>
	    <artifactId>graphql-java-tools</artifactId>
	    <version>13.0.3</version>
	</dependency>

接著在 web.xml 裡設定把流量導向 Spring MVC:

/src/main/webapp/WEB-INF/web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/mvc-config.xml</param-value>
		</init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name> 
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

設定 Spring MVC 的設定檔:

src/main/webapp/WEB-INF/mvc-config.xml:

<beans xmlns="http://www.springframework.org/schema/beans"  xmlns:security="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:util="http://www.springframework.org/schema/util"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task-3.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
        http://www.springframework.org/schema/cache 
        http://www.springframework.org/schema/cache/spring-cache.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util-3.0.xsd
        http://www.springframework.org/schema/oxm 
        http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-4.2.xsd"       
        >
                           	
	<context:component-scan base-package="com.*"/>
	<aop:aspectj-autoproxy/>
	<mvc:annotation-driven/>
</beans>

說明:把 GraphQL 的 Schema 設定在 schema.graphqls 中。

/src/main/resources/schema.graphqls:

scalar Date

type Book {
	id: ID!
	name: String
    publishDate: Date
}

type Author {
	id: ID!
	name: String
	bookList(prefix: String): [Book]
}

type Query {
  getAuthorById(id: ID!): Author
}

type Mutation {
    updateAuthorName(id: ID!, name: String!): Author
}

再來建好需要的 Bean 。

/src/java/com/bean/Author.java:

package com.bean;

import java.util.List;

public class Author {
	private int id;
	private String name;
	private List<Integer> bookIdList;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List<Integer> getBookIdList() {
		return bookIdList;
	}

	public void setBookIdList(List<Integer> bookIdList) {
		this.bookIdList = bookIdList;
	}
	
	
}

/src/java/com/bean/Book.java:

package com.bean;

import java.util.Calendar;

public class Book {
	private int id;
	private String name;
    Calendar publishDate;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
    
    public Calendar getPublishDate() {
		return publishDate;
	}

	public void setPublishDate(Calendar publishDate) {
		this.publishDate = publishDate;
	}
}

建好查詢資料和修改資料的 DAO (這邊沒有使用真正的資料庫,只是建假的資料來模擬)。

/src/main/java/com/dao/AuthorDAO.java:

package com.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.bean.Author;

@Repository
public class AuthorDAO {
	private static List<Author> authorList;
	
	public AuthorDAO() {
		authorList  = new ArrayList<Author>();
		
		//Set mock data to simulate data in database.
		Author author = new Author();
		author.setId(1);
		author.setName("Author 1");
		author.setBookIdList(List.of(1, 11));
		authorList.add(author);
		
		author = new Author();
		author.setId(2);
		author.setName("Author 2");
		author.setBookIdList(List.of(2, 22));
		authorList.add(author);
		
		author = new Author();
		author.setId(3);
		author.setName("Author 3");
		author.setBookIdList(List.of(3, 33));
		authorList.add(author);
	}
	
	public Author getAuthorById(int id) {
		return authorList.stream().filter((Author author) -> {
			return author.getId() == id;
		}).findAny().orElse(null);
	}
	
	public Author updateAuthorName(int id, String name) {		
		Author author = getAuthorById(id);
		if (author != null) {
			author.setName(name);
		}
		return author;
	}
}

/src/main/java/com/dao/BookDAO.java:

package com.dao;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;

import com.bean.Author;
import com.bean.Book;

@Repository
public class BookDAO {
	private static List<Book> bookList;
	
	public BookDAO() {
		bookList = new ArrayList<Book>();
		
		//Set mock data to simulate data in database.
		Book book = new Book();
		book.setId(1);
		book.setName("Book 1");
		book.setPublishDate(getCalendarByStr("2019-01-01T01:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(11);
		book.setName("Book 11");
		book.setPublishDate(getCalendarByStr("2019-01-01T01:01:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(2);
		book.setName("Book 2");
		book.setPublishDate(getCalendarByStr("2019-01-01T02:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(22);
		book.setName("Book 22");
		book.setPublishDate(getCalendarByStr("2019-01-01T02:02:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(3);
		book.setName("Book 3");
		book.setPublishDate(getCalendarByStr("2019-01-01T03:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(33);
		book.setName("Book 33");
		book.setPublishDate(getCalendarByStr("2019-01-01T03:03:00+0800"));
		bookList.add(book);
	}
	
	public Book getBookById(int id) {
		return bookList.stream().filter((Book book) -> {
			return book.getId() == id;
		}).findAny().orElse(null);
	}
	
	public List<Book> getBookListByAuthor(Author author) {
		if (author == null || author.getBookIdList() == null || author.getBookIdList().size() == 0) {
			return new ArrayList<Book>();
		}
		
		return author.getBookIdList().stream().map((Integer bookId) -> {
			return bookList.stream().filter((Book book) -> {
				return book.getId() == bookId;
			}).findAny().orElse(null);
		}).filter((Book book) -> {
			return book != null;
		})
		.collect(Collectors.toList());
	}
    
    Calendar getCalendarByStr(String dateStr) {
    	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
    	Calendar date = Calendar.getInstance();
    	try {
			date.setTime(sdf.parse(dateStr));
		} catch (ParseException e) {
			e.printStackTrace();
		}
        return date;
	}
}

我們這邊建一個 GraphqlController.java 來對應至GraphQL server 會收到的 request body JSON,
等一下在 Spring MVC 的 controller 可以直接把 GraphQL request 的值用 GraphqlController.java 來接,
這邊說明一下,Client 端要向 GraphQL 發起請求的時候,可以使用 HTTP GET 及 HTTP POST 的方式:

  1. HTTP GET:
    將參數直接串在網址後面,例如以下例子,不過要注意有些限制,像是無法使用 variables、mutation (雖然官方這樣說,不過其實如果 GraphQL server 是像這篇例子一樣自己實做的話,還是可以自己去設計取得 variables, mutation 實作行為,例如 query=mutation {...}) 等,可參考 Making a GraphQL requests using the GET method
    https://xxx/xxx?query=...&operationName=...
    
  2. HTTP POST:
    將參數 (query、operationName, variables 等) 直接放在 HTTP Body 中 (相當於 Postman 的 raw 格式),例如:
    {
      "query": "...",
      "operationName": "...",
      "variables": { "myVariable": "someValue", ... }
    } 

詳細可以參考這裡的說明: HTTP Methods, Headers, and Body

而 GraphqlController.java 我們把它設計成可對應 HTTP POST Body  的 JSON (有 query, operationName, variables 等屬性)。

/src/main/java/com/query/GraphqlRequestQuery.java:

package com.query;

import java.util.Map;

public class GraphqlRequestQuery {
	String query;
	String operationName;	
	Map<String, Object> variables = new HashMap<>();

	public String getQuery() {
		return query;
	}

	public void setQuery(String query) {
		this.query = query;
	}

	public String getOperationName() {
		return operationName;
	}

	public void setOperationName(String operationName) {
		this.operationName = operationName;
	}

	public Map<String, Object> getVariables() {
		return variables;
	}

	public void setVariables(Map<String, Object> variables) {
		this.variables = variables;
	}
}


建立各個 Resolver 和 Mutation,這會分別對應到 GraphQL 的 query 和 mutation,
說明都寫在注釋裡。

QueryResolver.java 對應到 GraphQL query,裡面會實作 getAuthorById() 和 getBookById() 方法

/src/main/java/com/resolver/QueryResolver.java:

package com.resolver;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.bean.Book;
import com.dao.AuthorDAO;
import com.dao.BookDAO;

import graphql.kickstart.tools.GraphQLQueryResolver;

@Component //對應至 GraphQL request 的 query
public class QueryResolver implements GraphQLQueryResolver {
	
	@Autowired
	AuthorDAO authorDAO;
	
	@Autowired
	BookDAO bookDAO;
	
	//對應至 GraphQL request query 的 getAuthorById
	public Author getAuthorById(int id) {
        return authorDAO.getAuthorById(id);
    }
	
	//對應至 GraphQL request query 的 getBookById
	public Book getBookById(int id) {
        return bookDAO.getBookById(id);
    }
} 

AuthorResolver.java 對應到 GraphQL query 回傳 Author 的 bookList 屬性實作方法,
因為在 QueryResolver.java 中 getAuthorById() 回傳的 Author 裡面找不到 bookList 屬性 (Author.java 只有 bookIdList 和 getBookIdList() 的 getter ,並且 bookIdList 是 int[],也不是 Book[] ),
所以我們必須為 Author 的 bookList 屬性實作值的取得方法。

/src/main/java/com/resolver/AuthorResolver.java:

package com.resolver;

import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.bean.Book;
import com.dao.BookDAO;

import graphql.kickstart.tools.GraphQLResolver;

@Component //對應至 GraphQL request query 中,Author 屬性值的 Resolver 獲取方式 
public class AuthorResolver implements GraphQLResolver<Author> {
	
	@Autowired
	BookDAO bookDAO;
	
	//對應至 GraphQL request query 中,Author 屬性 bookList 值的 Resolver 獲取方式 
	List<Book> bookList(Author author, String prefix) { //可獲取 Parent (也就是 Author) 的資料和傳入參數 (此例為 prefix)
		List<Book> bookList = bookDAO.getBookListByAuthor(author);
		
		if (StringUtils.isNotBlank(prefix)) {			
			bookList = bookList.stream().map((Book book) -> {
				Book renamedBook = new Book();
				renamedBook.setId(book.getId());
				renamedBook.setName(prefix + "-" + book.getName());
				renamedBook.setPublishDate(book.getPublishDate());
				return renamedBook;
			}).collect(Collectors.toList());
		}
		
		return bookList;
	}
}

MutationResolver.java 對應至 GraphQL mutation,裡面會實作 updateAuthorName

/src/main/java/com/resolver/MutationResolver.java:

package com.resolver;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.dao.AuthorDAO;

import graphql.kickstart.tools.GraphQLMutationResolver;

@Component //對應至 GraphQL request 的 mutation
public class MutationResolver implements GraphQLMutationResolver{
	
	@Autowired
	AuthorDAO authorDAO;
	
	//對應至 GraphQL request mutation 的 updateAuthorName
	public Author updateAuthorName(int id, String name) {
		return authorDAO.updateAuthorName(id, name);
	}
}

接下來我們要來處理自訂的 Date Scalar , 我們這邊建立一個 DateScalarBuilder.java 來回傳自訂實作的 GraphQLScalarType,關於 parseValue() 和 parseLiteral() 的差異可以參考官方這篇 Scalars in GraphQL 和 Stackoverflow 這篇 what's the difference between parseValue and parseLiteral in  GraphQLScalarType 別人的回答
DateScalarBuilder.java:

package com.scalar;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.GraphQLScalarType;

public class DateScalarBuilder {
	
	private DateScalarBuilder() {
		
	}
	
	public static GraphQLScalarType getDateScalar() {
		String dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ";
		
		return GraphQLScalarType.newScalar()
				.name("Date")
				.description("A custom scalar that handles date.")
		        .coercing(new Coercing<Calendar, String>() {
		            @Override
		            public String serialize(Object dataFetcherResult) {
                    	//實作從 Java 物件 (此例是 Calendar) 轉成要回傳 graphql response 的 String
		            	Calendar date = (Calendar) dataFetcherResult;
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		                return sdf.format(date.getTime()) ;
		            }

		            @Override
		            public Calendar parseValue(Object input) {
                    	//實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar)
                        //如果 input 使用了 inline input 的方式,就會使用 parseValue() 來解析
		            	//例如:
		            	/*
		            	 * Request:
		            	 * query myQuery {		            	 * 
		            	 * 	getBookById(id: 1) {
		            	 *  	id
		            	 *  	name 
		            	 * 	}
		            	 * } 
		            	 */
		            	String dateStr = (String) input;
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		            	Calendar date = Calendar.getInstance();
		            	try {
							date.setTime(sdf.parse(dateStr));
						} catch (ParseException e) {
							e.printStackTrace();
						}
		                return date;
		            }

		            @Override
		            public Calendar parseLiteral(Object input) {
                    	//實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar)
                        //如果 input 使用了 variables 的方式,就會使用 parseLiteral() 來解析
		            	//例如:
		            	/*
		            	 * Request:
		            	 * query myQuery($id: ID) { 
		            	 * 
		            	 * 	getBookById(id: $id) {
		            	 *  	id
		            	 *  	name 
		            	 * 	}
		            	 * }
		            	 * 
		            	 * Variables:
		            	 * {
		            	 * 	"id": 1
		            	 * }
		            	 */
		            	String dateStr = ((StringValue) input).getValue();
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		            	Calendar date = Calendar.getInstance();
		            	try {
							date.setTime(sdf.parse(dateStr));
						} catch (ParseException e) {
							e.printStackTrace();
						}
		                return date;
		            }
		        })
		        .build();
	}
}

GraphqlController.java 是 SpringMVC 的 Controller 設定,
這裡我們設定了 /graphql 為處理 GraphQL request 的 url,
限制只接收 HTTP POST 且 Mime type 為 application/json 格式的 request。

接著讀取 GraphQL Schema 、設定要用的 Resolver 、 Scalar,並配合接收到的 GraphQL request
產生並回傳查詢結果。

src/java/com/controller/GraphqlController.java:

package com.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.kickstart.tools.SchemaParser;
import graphql.schema.GraphQLSchema;

import com.scalar.DateScalarBuilder;
import com.query.GraphqlRequestQuery;
import com.resolver.AuthorResolver;
import com.resolver.MutationResolver;
import com.resolver.QueryResolver;

@Controller
public class GraphqlController {
	
	@Autowired
	private QueryResolver queryResolver;
	
	@Autowired
	private AuthorResolver authorResolver;
	
	@Autowired
	private MutationResolver mutationResolver;
	
	@RequestMapping(value="/graphql", method = {RequestMethod.POST}, produces = "application/json")
	public @ResponseBody Map<String, Object> myGraphql(@RequestBody GraphqlRequestQuery requestQuery) {
		
		//讀取 GraphQL Schema、設定各 Resolver
		GraphQLSchema graphQLSchema = SchemaParser.newParser()
									  .file("schema.graphqls")
                                      .scalars(DateScalarBuilder.getDateScalar())
									  .resolvers(queryResolver
											     , authorResolver
											     , mutationResolver)
									  .build()
									  .makeExecutableSchema();

		GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build();
		
		//將 GraphQL request 的 query (也可視需要取出 operationName, variables) 值取出,準備進行查詢
		ExecutionInput executionInput = ExecutionInput.newExecutionInput()
										.query(requestQuery.getQuery())
                                        .operationName(requestQuery.getOperationName())
                                        .variables(requestQuery.getVariables())
										.build();
		ExecutionResult executionResult = graphQL.execute(executionInput);	
		
		//executionResult.toSpecification() 會回傳一個 Map,
		//裡面包含了查詢結果 data 、 errors、 extensions 等資料
		return executionResult.toSpecification();
	}
}

最後展示一下查詢結果:

  1. 查詢:
    query {
      getAuthorById(id: 3) {
        id
        name
        bookList(prefix: "test") {
            id
            name
            publishDate
        }
      },
      getBookById(id: 33) {
        id
        name
        publishDate
      }
    }
    
    結果:
    {
        "data": {
            "getAuthorById": {
                "id": "3",
                "name": "Author 3",
                "bookList": [
                    {
                        "id": "3",
                        "name": "test-Book 3"
                        "publishDate": "2019-01-01T03:00:00+0800"
                    },
                    {
                        "id": "33",
                        "name": "test-Book 33"
                        "publishDate": "2019-01-01T03:03:00+0800"
                    }
                ]
            },
            "getBookById": {
                "id": "33",
                "name": "Book 33"
                "publishDate": "2019-01-01T03:03:00+0800"
            }
        }
    }
    
  2. 查詢:
    mutation {
        updateAuthorName(id: 3, name: "Author 3 - changed") {
            id
            name
            bookList(prefix: "test") {
                id
                name
                publishDate
            }
        }
    }
    
    結果:
    {
        "data": {
            "updateAuthorName": {
                "id": "3",
                "name": "Author 3 - changed",
                "bookList": [
                    {
                        "id": "3",
                        "name": "test-Book 3"
                        "publishDate": "2019-01-01T03:00:00+0800"
                    },
                    {
                        "id": "33",
                        "name": "test-Book 33"
                        "publishDate": "2019-01-01T03:03:00+0800"
                    }
                ]
            }
        }
    }
    

下載分享:

MyGraphQL.7z

參考資料:

  1. HTTP Methods, Headers, and Body
  2. Making a GraphQL requests using the GET method
  3. GraphQL x Spring WebMVC
  4. Defining a schema
  5. Think in GraphQL :: 2019 iT 邦幫忙鐵人賽

沒有留言 :

張貼留言