SpringBoot + GraphQL + Mysql 实现 CRUD

spring-boot-graphql-mysql-crud-apis-feature-image

GraphQL是一种查询语言用来设计 APIs,可替代 REST,SOAP 或 gRPC,详细的可以查看官网解释: GraphQL

本教程中,我们将构建一个 Spring Boot GraphQL 例子并提供 CRUD APIs 来从 mysql 数据库中创建,查询,更新,删除对象。

Sring Boot CRUD GraphQL APIs 概览

目标

我们有两个数据模型:作者(Author)课程(Tutorial)

1
2
3
4
5
6
7
8
9
10
11
12
Author {
id: Long
name: String
age: Integer
}

Tutorial {
id: Long
title: String
description: String
author: Author
}

一个作者可能有多个课程 , 所以他们是一对多的关系

CRUD GraphQL APIs

  • 创建作者:

    • GraphQL

      1
      2
      3
      4
      5
      6
      7
      8
      mutation {
      createAuthor(
      name: "bezkoder",
      age: 27) {
      id
      name
      }
      }
    • 响应

      1
      2
      3
      4
      5
      6
      7
      8
      {
      "data": {
      "createAuthor": {
      "id": "1",
      "name": "bezkoder"
      }
      }
      }
  • 查询所有作者

    • GraphQL

      1
      2
      3
      4
      5
      6
      7
      query {
      findAllAuthors{
      id
      name
      age
      }
      }
    • 响应

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      {
      "data": {
      "findAllAuthors": [
      {
      "id": 1,
      "name": "张三",
      "age": 25
      },
      {
      "id": 2,
      "name": "李四"
      "age": 43
      }
      ]
      }
      }

    等等,这里先不一一列举了

如果查看数据库,你会发现有两张表: authortutorial

内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select * from author;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 25 | 张三 |
| 2 | 43 | 李四 |
+----+-----+--------+

mysql> select * from tutorial;
+----+-------------------------------------+----------------+-----------+
| id | description | title | author_id |
+----+-------------------------------------+----------------+-----------+
| 1 | 该教程可以帮助你学习 Java | 学习 Java | 1 |
| 2 | 很棒的 GraphQL 教程 | 学习 GraphQL | 2 |
+----+-------------------------------------+----------------+-----------+

开始构建APIs

用到的依赖

我们的应用将用到下面这些:

  • Java8 或者 java11
  • Spring Boot 2.2.1.RELEASE (with Spring Web)
  • Graphql-spring-boot-starter 7.0.1
  • Graphiql-spring-boot-starter 7.0.1
  • Gradle 6.3
  • MySQL 5.7

项目结构

image-20200426170523001

  • resources/graphql 中所有的 .graphqls 定义了 GraphQL schemas
  • model: 存放 Author 和 Tutorial 实体类
  • mapper: 存放 Mapper 接口来和 MySQL 交互
  • resolver: 通过实现一些 Resolver接口来处理查询和修改的请求,该类可以理解成 SpringMVC 中的 Controller 层
  • application.yml: 配置文件
  • build.gradle: gradle 的构建文件

项目准备

首先创建 Spring Boot 项目,然后添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
dependencies {
// SpringBoot
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}

// Graphql
implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.0.1'
implementation 'com.graphql-java-kickstart:graphiql-spring-boot-starter:7.0.1'

// Mybatis-Plus
implementation 'com.baomidou:mybatis-plus-boot-starter:3.1.2'

// Mysql
implementation 'mysql:mysql-connector-java:5.1.6'

// Flyway
implementation 'org.flywaydb:flyway-core:6.3.3'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Spock
testImplementation 'org.spockframework:spock-core:2.0-M1-groovy-2.5'
testImplementation 'org.spockframework:spock-spring:2.0-M1-groovy-2.5'
}

相关配置文件

打开 application.yml 并添加下面的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8083

spring:
flyway:
locations: classpath:db/migration
out-of-order: true
url: ${spring.datasource.url}
user: ${spring.datasource.username}
password: ${spring.datasource.password}
datasource:
url: jdbc:mysql://localhost:3306/learn_graphql
username: root
password:
driver-class-name: com.mysql.jdbc.Driver

mybatis-plus:
type-aliases-package: com.kba977.learngraphql.mapper

创建 GraphQL Schema

我们为了将schema拆分到不同的文件里,以分类管理,Spring Boot GraphQL 会自动扫描所有 schema 文件

首先我们创建 Schemas

author.graphqls

1
2
3
4
5
6
7
8
9
10
type Author {
id: ID!
name: String!
age: Int
}

input CreateAuthorInput {
name: String!
age: Int!
}

tutorial.graphqls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Tutorial {
id: ID!
title: String!
description: String
author: Author
}

input CreateTutorialInput {
title: String!
description: String!
authorId: ID!
}

input UpdateTutorialInput {
title: String
description: String
}

query.graphqls

1
2
3
4
5
6
7
8
9
10
11
12
# Root
type Query {
# Author
findAllAuthors: [Author]!
findAuthorById(id: ID!): Author
countAuthors: Int!

# Tutorial
findAllTutorials: [Tutorial]!
findTutorialById(id: ID!): Tutorial
countTutorials: Int!
}

mutation.graphqls

1
2
3
4
5
6
7
8
9
10
# Root
type Mutation {
# Author
createAuthor(createAuthorInput: CreateAuthorInput!): Author!

# Tutorial
createTutorial(createTutorialInput: CreateTutorialInput!): Tutorial!
updateTutorial(id: ID!, updateTutorialInput: UpdateTutorialInput): Tutorial!
deleteTutorial(id: ID!): Boolean
}

在 GraphQL 中分为两个大类,所有的查询入口在 Query, 所有的变更入口都在 Mutation

上面文件中 ! 的意思该字段不能为 null,如果加 ! ,那么在返回值的时候 GraphQL允许返回 null

定义实体类,Mapper接口,Service实现

这些我们比较熟悉,直接跳过大家可以查看源码

实现GraphQL Root 解析器

如下面文件 TutorialMutationResolver 实现了对 Tutorial 的变更请求处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.kba977.learngraphql.graphql.resolver.mutation;

import com.kba977.learngraphql.graphql.type.tutorial.CreateTutorialInput;
import com.kba977.learngraphql.graphql.type.tutorial.UpdateTutorialInput;
import com.kba977.learngraphql.model.Tutorial;
import com.kba977.learngraphql.service.TutorialService;
import graphql.kickstart.tools.GraphQLMutationResolver;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class TutorialMutationResolver implements GraphQLMutationResolver {

@Autowired
private TutorialService tutorialService;

public Tutorial createTutorial(CreateTutorialInput input) {
Tutorial tutorial = new Tutorial(input.getTitle(), input.getDescription(), input.getAuthorId());
tutorialService.save(tutorial);
return tutorial;
}

public Tutorial updateTutorial(Integer id, UpdateTutorialInput input) throws NotFoundException {
Optional<Tutorial> optTutorial = Optional.ofNullable(tutorialService.getById(id));

if (optTutorial.isPresent()) {
Tutorial tutorial = optTutorial.get();
BeanUtils.copyProperties(input, tutorial);
tutorialService.updateById(tutorial);

return tutorialService.getById(id);
}
throw new NotFoundException("Not found Tutorial to update!");
}

public boolean deleteTutorial(Integer id) {
return tutorialService.removeById(id);
}
}

实现 GraphQL 字段解析器

对于 Tutorial 中的复杂类型 author,我们需要字段解析器去解析它的值,TutorialResolver 实现了 GraphQLResolver 接口并实现 getAuthor() 方法

resolver/TutorialResolver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kba977.learngraphql.graphql.resolver;

import com.kba977.learngraphql.model.Author;
import com.kba977.learngraphql.model.Tutorial;
import com.kba977.learngraphql.service.AuthorService;
import graphql.kickstart.tools.GraphQLResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class TutorialResolver implements GraphQLResolver<Tutorial> {

@Autowired
private AuthorService authorService;

public Author getAuthor(Tutorial tutorial) {
return authorService.getById(tutorial.getAuthorId());
}
}

如果客户端请求 Tutorial 没有带上 author 字段,那么 GraphQL Server 将不会调用该方法去解析字段

运行应用并检查结果

应用启动后,打开浏览器访问 localhost:8083/graphiql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
fragment author on Author {
id
name
age
}

query findAllAuthors {
findAllAuthors {
...author
}
}

query findAuthorById {
findAuthorById(id: 3) {
...author
}
}

query countAuthors {
countAuthors
}

fragment tutorial on Tutorial {
id
title
description
author {
...author
}
}

query findAllTutorials {
findAllTutorials {
...tutorial
}
}

query countTutorials {
countTutorials
}

mutation createAuthor {
createAuthor(createAuthorInput: {
name: "kba999"
age: 20 })
{
...author
}
}

mutation createTutorial {
createTutorial(createTutorialInput: {
title: "新概念英语"
description: "学习英语的好资料"
authorId: 2
}) {
...tutorial
}
}

mutation updateTutorial {
updateTutorial(id: 4, updateTutorialInput: {
title: "我们的世界"
}) {
...tutorial
}
}

mutation deleteTutorial {
deleteTutorial(id: 1)
}

image-20200426185338528

进一步学习资源

源代码

你可以从github上找到完整的代码:https://github.com/kba977/learn-graphql