GraphQL api服务器

选择 GraphQL 的好处?

  • 避免对 API 进行多个版本控制
  • 客户端可以只获取其需要的字段,而服务端不用做特殊的处理
  • 避免多次调用API以获取相关联数据。GraphQL允许了在单个请求中获取其相关联数据
  • 提高其程序性能

GraphQL 架构

  1. Schema: 定义了数据的类型、数据之间的关系以及允许客户端进行的操作,以及每个操作的参数和返回类型。
  2. Queries: 用于从GraphQL服务器检索数据。
  3. Mutations: 用于对服务器进行写操作,比如新增、修改和删除等操作。

使用 golang Gin 创建 GraphQL api 服务器

1
2
3
4
5
6
7
$ echo '初始项目环境'
$ mkdir graphql_test_server
$ cd graphql_test_server
$ go mod init github.com/sy-vendor/graphql_test_server
$ go get github.com/99designs/gqlgen
$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/jinzhu/gorm
1
2
$ echo '构建服务器'
$ go run github.com/99designs/gqlgen init

编写 GraphQL

文件graph/schema.graphqls

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
type Question{
id: String!
question_text: String!
pub_date: String!
choices: [Choice]
}

type Choice{
id: String!
question: Question!
question_id: String!
choice_text: String!
}

type Query {
questions: [Question]!
choices: [Choice]!
}

input QuestionInput {
question_text: String!
pub_date: String!
}

input ChoiceInput {
question_id: String!
choice_text: String!
}

type Mutation {
createQuestion(input: QuestionInput!): Question!
createChoice(input: ChoiceInput): Choice!
}

运行下面的命令来更新schema实现

1
$ rm graph/schema.resolvers.go && gqlgen generate

打开 graph/schema.resolvers.go,查看是否像如下生成了查询func (r *queryResolver) Questionsfunc (r *queryResolver) Choices以及func (r *mutationResolver) CreateQuestionfunc (r *mutationResolver) CreateChoice

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
package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
"context"
"fmt"

"github.com/sy-vendor/gin-graphql-postgres/graph/generated"
"github.com/sy-vendor/gin-graphql-postgres/graph/model"
)

func (r *mutationResolver) CreateQuestion(ctx context.Context, input model.QuestionInput) (*model.Question, error) {
panic(fmt.Errorf("not implemented"))
}

func (r *mutationResolver) CreateChoice(ctx context.Context, input *model.ChoiceInput) (*model.Choice, error) {
panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Questions(ctx context.Context) ([]*model.Question, error) {
panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Choices(ctx context.Context) ([]*model.Choice, error) {
panic(fmt.Errorf("not implemented"))
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

使用’gorm’设置数据库orm

创建文件db/main.go并实现以下代码

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
package database

import (
"fmt"

"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"

"graphql_test_server/graph/model"
)

type dbConfig struct {
host string
port int
user string
dbname string
password string
}

var config = dbConfig{"localhost", 5432, "postgres", "test", "root"}

func getDatabaseUrl() string {
return fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s password=%s",
config.host, config.port, config.user, config.dbname, config.password)
}

func GetDatabase() (*gorm.DB, error) {
db, err := gorm.Open("postgres", getDatabaseUrl())
return db, err
}

func RunMigrations(db *gorm.DB) {
if !db.HasTable(&model.Question{}) {
db.CreateTable(&model.Question{})
}
if !db.HasTable(&model.Choice{}) {
db.CreateTable(&model.Choice{})
db.Model(&model.Choice{}).AddForeignKey("question_id", "questions(id)", "CASCADE", "CASCADE")
}
}

完善 GraphQL 解析器

编写graph/schema.resolvers.go

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
72
73
74
75
76
77
78
79
80
81
82
package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
"context"
"fmt"
"log"

database "graphql_test_server/db"
"graphql_test_server/graph/generated"
"graphql_test_server/graph/model"
)

func (r *mutationResolver) CreateQuestion(ctx context.Context, input model.QuestionInput) (*model.Question, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
fmt.Println("input", input.QuestionText, input.PubDate)
question := model.Question{}
question.QuestionText = input.QuestionText
question.PubDate = input.PubDate
db.Create(&question)
return &question, nil
}

func (r *mutationResolver) CreateChoice(ctx context.Context, input *model.ChoiceInput) (*model.Choice, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
choice := model.Choice{}
question := model.Question{}
choice.QuestionID = input.QuestionID
choice.ChoiceText = input.ChoiceText
db.First(&question, choice.QuestionID)
choice.Question = &question
db.Create(&choice)
return &choice, nil
}

func (r *queryResolver) Questions(ctx context.Context) ([]*model.Question, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
db.Find(&r.questions)
for _, question := range r.questions {
var choices []*model.Choice
db.Where(&model.Choice{QuestionID: question.ID}).Find(&choices)
question.Choices = choices
}
return r.questions, nil
}

func (r *queryResolver) Choices(ctx context.Context) ([]*model.Choice, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
db.Find(&r.choices)
return r.choices, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

最后编写server.go

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
package main

import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gin-gonic/gin"

"graphql_test_server/graph"
"graphql_test_server/graph/generated"
)

const defaultPort = ":8080"

// Defining the Graphql handler
func graphqlHandler() gin.HandlerFunc {
// NewExecutableSchema and Config are in the generated.go file
// Resolver is in the resolver.go file
h := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}

// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
h := playground.Handler("GraphQL", "/query")

return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}

func main() {
r := gin.Default()
r.POST("/query", graphqlHandler())
r.GET("/", playgroundHandler())
r.Run(defaultPort)
}

现在使用如下命令准备测试 GraphQL api 服务器

1
$ go run server.go

GraphQL 中 mutation

1
2
3
4
5
6
mutation {
createQuestion(input: {question_text: "What is your name ?", pub_date: "2023-03-12"}){
id
question_text
}
}

执行上述查询后,将得到如下所示的 JSON 数据

1
2
3
4
5
6
7
8
{
"data": {
"createQuestion": {
"id": "3",
"question_text": "What is your name ?"
}
}
}

在上面用数据创建{question_text: "What is your name ?", pub_date: "2020-04-27"},创建后要求它返回idquestion_text。如果只想要 id 那么可以question_text从查询中删除。

相应的cURL请求如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl 'http://localhost:8080/query' \
-H 'Connection: keep-alive' \
-H 'accept: */*' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36' \
-H 'content-type: application/json' \
-H 'Origin: http://localhost:8080' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Referer: http://localhost:8080/' \
-H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8'\
--data-binary '{"operationName":null,"variables":{},"query":"mutation {\n createQuestion(input: {question_text: \"What is your name ?\", pub_date: \"2020-03-12\"}) {\n id\n question_text\n }\n}\n"}' \
--compressed

同样方式创建Choice:

1
2
3
4
5
6
7
8
9
10
mutation {
createChoice(input: {question_id: "3", choice_text: "Agiliq"}){
id
question{
id
question_text
}
choice_text
}
}

GraphQL 中 query

编写一个 graphql 的query

1
2
3
4
5
6
7
8
9
10
query{
questions{
id
question_text
choices{
id
choice_text
}
}
}

按上述query条件请求将返回如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data": {
"questions": [
{
"id": "3",
"question_text": "What is your name ?",
"choices": [
{
"id": "31",
"choice_text": "Agiliq"
}
]
}
]
}
}

源码:  Github - graphql_test_server