在实现一个GraphQL Server之前,建议大家先去看看一个具体的GraphQL查询语句是如何执行的,由于理论的内容太多,请大家具体参阅:
https://graphql.org/learn/execution/
本章节项目案例及部分代码参考自:
https://www.graphql-java.com/documentation/v14/
SpringBoot应该是目前基于Java语言的最重要的一个框架了,也是Java开源框架中的一个具有划时代意义的产品。结合Java面向对象的语言特性和Spring的AOP软件工程思想,应该说SpringBoot是开发大型、复杂的前后端分离的软件的不二选择。特别是由Java衍生出的Scala结合Spark、Hadoop等大数据平台,为打造能力更强的“大后端”提供了可能性。
下面我们就结合一个具体的例子,来看看如何使用SpringBoot来开发一个我们自己的GraphQL Server。
Java开发工具:IntellJ IDEA,一个能够让我这个用了十多年Eclipse的转投IntellJ自然尤其自身的优势。
数据库:MySQL
数据库管理工具:MySQL Workbench
CREATE DATABASE /*!32312 IF NOT EXISTS*/`graphql` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `graphql`;
/*Table structure for table `author` */
DROP TABLE IF EXISTS `author`;
CREATE TABLE `author` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Key word',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Created time',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Updated time',
`first_name` varchar(50) DEFAULT NULL COMMENT 'firstName',
`last_name` varchar(50) DEFAULT NULL COMMENT 'lastName',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/*Table structure for table `book` */
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Key word',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Created time',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Updated time',
`title` varchar(50) DEFAULT NULL COMMENT 'Title',
`author_id` bigint(20) NOT NULL,
`isbn` varchar(255) DEFAULT NULL,
`page_count` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
执行完上述代码之后,在MySQL中能创建一个名为graphql的database。它包含两个表格“author”和“book”。
创建完成之后,schema的结构如下:
在IntellJ中,选择File=>new=>project,在new project对话框中,选择“Spring Initia”,在“Project SDK”中建议选择“1.8”,因为本人在实际开发过程中SDK 11曾经出现过很多兼容性问题。
单击“Next”
在这一步就按默认的选项,Java version建议选“8”,单击“Next”
在这一步我们需要选择项目当中需要用的的dependecies:
在本项目中,需要用到以下dependencies:
File=>setting,在setting对话框中选择plugins,确认添加了JS GraphQL plugin
目前在SpringBoot中对GraphQL支持比较好的Package就是graphql-java-kickstart了,它集成了相当多的有用工具,比原生的GraphQL-Java package要好用很多,而且在其中就集成了GraphQL Playground调试工具,测试GraphQL语句非常方便。详细请参阅:https://github.com/graphql-java-kickstart/graphql-spring-boot
添加完上述所需dependencies后,完整的pom.xml文件内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.0-SNAPSHOTversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>springboot_graphqlartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot_graphqlname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>8java.version>
<kotlin.version>0.3.70kotlin.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>3.4.5version>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>graphql-spring-boot-starterartifactId>
<version>8.0.0version>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>altair-spring-boot-starterartifactId>
<version>8.0.0version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>graphiql-spring-boot-starterartifactId>
<version>8.0.0version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>voyager-spring-boot-starterartifactId>
<version>8.0.0version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>graphql-spring-boot-starter-testartifactId>
<version>8.0.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>com.graphql-java-kickstartgroupId>
<artifactId>playground-spring-boot-starterartifactId>
<version>8.0.0version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.github.graphql-javagroupId>
<artifactId>graphql-java-annotationsartifactId>
<version>8.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.14version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.coxautodevgroupId>
<artifactId>graphql-java-toolsartifactId>
<version>2.1.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
repository>
<repository>
<id>spring-snapshotsid>
<name>Spring Snapshotsname>
<url>https://repo.spring.io/snapshoturl>
<snapshots>
<enabled>trueenabled>
snapshots>
repository>
<repository>
<id>jcenterid>
<url>https://jcenter.bintray.com/url>
repository>
repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
pluginRepository>
<pluginRepository>
<id>spring-snapshotsid>
<name>Spring Snapshotsname>
<url>https://repo.spring.io/snapshoturl>
<snapshots>
<enabled>trueenabled>
snapshots>
pluginRepository>
pluginRepositories>
project>
config目录:存放config配置文件 entity目录:存放entity实体类文件,entity实体类与MySQL数据库中表
model目录:存放数据model文件 repo目录:repository目录,存放对数据模型操作文件
resolver目录:这是GraphQL
server项目中最为重要的目录,里面的文件负责将前端的GraphQL操作转换成真实的、基于SpringBoot的数据库操作。
同时,在项目的resources目录中创建graphql目录,用于存放与GraphQL相关的.graphql文件:
当成功创建一个SpringBoot项目时,系统会自动为我们创建一个application.properties文件,主要用于系统运行时用到的环境参数变量以及相关的值,在这里我们用自己定义的application.yml文件,相比较.properties文件,.yml文件的层次更加清晰也更简洁,其内容如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/graphql?serverTimezone=UTC
username: root
password: 1234
# driver-class-name: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 60000
max-lifetime: 1800000
connection-timeout: 30000
data-source-properties:
cachePreStmts: true
preStmtCacheSize: 250
main:
allow-bean-definition-overriding: true
# dbcp2:
# driver-class-name: com.mysql.jdbc.Driver
# max-wait-millis: 20
# max-idle: 8
# min-idle: 8
# initial-size: 10
jpa:
hibernate:
ddl-auto: update
database: mysql
graphql:
servlet:
mapping: /graphql
enabled: true
corsEnabled: true
cors:
allowed-origins: http://some.domain.com
# if you want to @ExceptionHandler annotation for custom GraphQLErrors
exception-handlers-enabled: true
contextSetting: PER_REQUEST_WITH_INSTRUMENTATION
tools:
schema-location-pattern: "**/*.graphql"
# Enable or disable the introspection query. Disabling it puts your server in contravention of the GraphQL
# specification and expectations of most clients, so use this option with caution
introspection-enabled: true
annotations:
base-package: com.exaple.springboot_graphql # required
always-prettify: true #true is the default value, no need to specify it
graphql.playground:
mapping: /playground
endpoint: /graphql
subscriptionEndpoint: /subscriptions
staticPath.base: my-playground-resources-folder
enabled: true
pageTitle: Playground
cdn:
enabled: false
version: latest
settings:
editor.cursorShape: line
editor.fontFamily: "'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace"
editor.fontSize: 14
editor.reuseHeaders: true
editor.theme: dark
general.betaUpdates: false
prettier.printWidth: 80
prettier.tabWidth: 2
prettier.useTabs: false
request.credentials: omit
schema.polling.enable: true
schema.polling.endpointFilter: "*localhost*"
schema.polling.interval: 2000
schema.disableComments: true
tracing.hideTracingResponse: true
headers:
headerFor: AllTabs
tabs:
- name: Example Tab
query: classpath:exampleQuery.graphql
headers:
SomeHeader: Some value
variables: classpath:variables.json
responses:
- classpath:exampleResponse1.json
- classpath:exampleResponse2.json
在文件中比较重要的配置内容有:
datasource: 配置数据库连接参数;
hikari: 配置数据库连接池,使用数据连接池可以大大优化对数据库的连接域访问速度,具体请参阅:https://www.baeldung.com/spring-boot-hikari
graphql:graphql的基本配置;
graphql.playground: playground调试工具的基本配置
entity目录中的文件为数据实体文件,该目录中的文件与数据库中的表对应:
在数据库中我们创建了author表和book表,因此我们就创建了Author.java和Book.java文件与之相对应。并且在author表和book表中分别都有created_time、updated_time以及Id字段,为避免冗余,我们创建了BaseEntity.java文件来对应这些字段。
各文件内容代码如下:
package com.exaple.springboot_graphql.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
/**
* @author comqiao
* @create 2020-11-12 17:18
*/
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {
/** ID */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "bigint", nullable = false)
protected Long id;
/** Create Timestamp (unit:second) */
@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
protected Date createdTime;
/** Update Timestamp (unit:second) */
@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
protected Date updatedTime;
public BaseEntity() {
createdTime = new Date();
updatedTime = createdTime;
}
@PreUpdate
private void doPreUpdate() {
updatedTime = new Date();
}
}
package com.exaple.springboot_graphql.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
public class Author extends BaseEntity {
@Column(columnDefinition = "varchar(50)")
private String firstName;
@Column(columnDefinition = "varchar(50)")
private String lastName;
}
package com.exaple.springboot_graphql.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
public class Book extends BaseEntity {
@Column(columnDefinition = "varchar(50)")
private String title;
private String isbn;
private int pageCount;
private long authorId;
}
这个目录中的数据model文件要与GraphQL中schema中所定义的数据操作相关联,例如我在schema中定义了一个type为input,名称为BookInput的model(Type definition的内容请查阅前面章节)
因此,在这个目录中,我们需要编写一个BookInput.java文件与之相对应,内容如下:
package com.exaple.springboot_graphql.model;
import lombok.Data;
@Data
public class BookInput {
private String title;
private String isbn;
private int pageCount;
private long authorId;
}
repo目录中的文件都是Interface文件,它们均继承自JpaRepository接口。在这些接口文件中定义了数据访问及操作接口。具体文件内容如下:
package com.exaple.springboot_graphql.repo;
import com.exaple.springboot_graphql.entity.Author;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author comqiao
* @create 10-10-2020 13:35
*/
public interface AuthorRepo extends JpaRepository<Author,Long> {
Author findAuthorById(Long id);
}
package com.exaple.springboot_graphql.repo;
import com.exaple.springboot_graphql.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* @author comqiao
* @create 2020-10-10 13:39
*/
public interface BookRepo extends JpaRepository<Book,Long> {
List<Book> findByAuthorId(Long id);
Book findBookById(Long id);
}
这个目录中的文件负责实现对数据库操作的具体实现,与以往的SpringBoot文件不同的是,这个目录中的问价还要负责将前端传递过来的GraphQL语句解析成具体的数据操作代码,因此该目录中有以下文件:
package com.exaple.springboot_graphql.resolver;
import com.exaple.springboot_graphql.entity.Author;
import com.exaple.springboot_graphql.entity.Book;
import graphql.kickstart.tools.GraphQLResolver;
import com.exaple.springboot_graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.List;
/**
* @author comqiao
* @create 2020-10-12 12:03
*/
@Component
@AllArgsConstructor
public class AuthorResolver implements GraphQLResolver<Author> {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private BookRepo bookRepo;
public String getCreatedTime(Author author) {
return sdf.format(author.getCreatedTime());
}
public List<Book> getBooks(Author author) {
return bookRepo.findByAuthorId(author.getId());
}
}
package com.exaple.springboot_graphql.resolver;
import graphql.kickstart.tools.GraphQLResolver;
import com.exaple.springboot_graphql.entity.Author;
import com.exaple.springboot_graphql.entity.Book;
import com.exaple.springboot_graphql.repo.AuthorRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author comqiao
* @create 2020-10-12 12:10
*/
@Component
@AllArgsConstructor
public class BookResolver implements GraphQLResolver<Book> {
private AuthorRepo authorRepo;
public Author getAuthor(Book book) {
return authorRepo.findAuthorById(book.getAuthorId());
}
}
package com.exaple.springboot_graphql.resolver;
import graphql.kickstart.tools.GraphQLMutationResolver;
import com.exaple.springboot_graphql.entity.Author;
import com.exaple.springboot_graphql.entity.Book;
import com.exaple.springboot_graphql.model.BookInput;
import com.exaple.springboot_graphql.repo.AuthorRepo;
import com.exaple.springboot_graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author comqiao
* @create 2019-10-14 12:46
*/
@Component
@AllArgsConstructor
public class Mutation implements GraphQLMutationResolver {
private AuthorRepo authorRepo;
private BookRepo bookRepo;
public Author newAuthor(String firstName, String lastName) {
Author author = new Author();
author.setFirstName(firstName);
author.setLastName(lastName);
return authorRepo.save(author);
}
public Book newBook(String title, String isbn, int pageCount, Long authorId) {
Book book = new Book();
book.setTitle(title);
book.setIsbn(isbn);
book.setPageCount(pageCount);
book.setAuthorId(authorId);
return bookRepo.save(book);
}
public Book saveBook(BookInput input) {
Book book = new Book();
book.setTitle(input.getTitle());
book.setIsbn(input.getIsbn());
book.setPageCount(input.getPageCount());
book.setAuthorId(input.getAuthorId());
return bookRepo.save(book);
}
public Boolean deleteBook(Long id) {
bookRepo.deleteById(id);
return true;
}
public Book updateBookPageCount(int pageCount,long id) {
Book book = bookRepo.findBookById(id);
book.setPageCount(pageCount);
return bookRepo.save(book);
}
}
该文件负责对数据进行更新操作,因此分别定义了save、delete和update操作
package com.exaple.springboot_graphql.resolver;
import graphql.kickstart.tools.GraphQLQueryResolver;
import com.exaple.springboot_graphql.entity.Author;
import com.exaple.springboot_graphql.entity.Book;
import com.exaple.springboot_graphql.repo.AuthorRepo;
import com.exaple.springboot_graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author comqiao
* @create 2020-10-17 12:40
*/
@Component
@AllArgsConstructor
public class Query implements GraphQLQueryResolver {
private AuthorRepo authorRepo;
private BookRepo bookRepo;
public Author findAuthorById(Long id) {
return authorRepo.findAuthorById(id);
}
public List<Author> findAllAuthors() {
return authorRepo.findAll();
}
public Long countAuthors() {
return authorRepo.count();
}
public List<Book> findAllBooks() {
return bookRepo.findAll();
}
public Long countBooks() {
return bookRepo.count();
}
}
该文件负责实现对数据的查询
该目录中的文件主要是完成GraphQL中的schema的类型定义,要让系统成功找到该定义文件,需要在前面所讲述的application.yml中定义该配置文件所在的位置:
该目录中主要有以下两个文件:
1.schema.graphql
该文件就是使用GraphQL的类型定义语言所定义的schema,是整个GraphQL功能实现的核心,其内容如下:
type Author{
id:ID!
createdTime:String
firstName:String
lastName:String
books:[Book]
}
input BookInput{
title:String!
isbn:String!
pageCount:Int
authorId:ID
}
type Book{
id:ID!
title:String
isbn:String
pageCount:Int
author:Author
}
2.root.graphql
该文件的重要性在前面章节已经介绍过,它负责定义在GraphQL的查询语言中实现的语言的格式样式,其内容如下:
type Query{
findAuthorById(id:ID!):Author
findAllAuthors:[Author]
countAuthors:Int
findAllBooks:[Book]!
countBooks:Int!
}
type Mutation {
newAuthor(firstName: String!,lastName: String!) : Author!
newBook(title: String!,isbn: String!,pageCount: Int, authorId: ID!) : Book!
saveBook(input: BookInput!) : Book!
deleteBook(id: ID!) : Boolean
updateBookPageCount(pageCount: Int!, id:ID!) : Book!
}
至此,基于SpringBoot的GraphQL Server代码编写全部完成,下一章节将讲解如何利用GraphQL Playground对server进行调试