| by suyi | No comments

GraphQL api服务器

选择 GraphQL 的好处?

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

GraphQL 架构

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

使用 golang Gin 创建 GraphQL api 服务器

$ 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
$ echo '构建服务器'
$ go run github.com/99designs/gqlgen init

编写 GraphQL

文件graph/schema.graphqls

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实现

$ 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

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并实现以下代码

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

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

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 服务器

$ go run server.go

GraphQL 中 mutation

mutation {
  createQuestion(input: {question_text: "What is your name ?", pub_date: "2023-03-12"}){
    id
    question_text
  }
}

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

{
  "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请求如下:

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:

mutation {
  createChoice(input: {question_id: "3", choice_text: "Agiliq"}){
    id
    question{
      id
      question_text
    }
    choice_text
  }
}

GraphQL 中 query

编写一个 graphql 的query

query{
  questions{
    id
    question_text
    choices{
      id
      choice_text
    }
  }
}

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

{
  "data": {
    "questions": [
      {
        "id": "3",
        "question_text": "What is your name ?",
        "choices": [
          {
            "id": "31",
            "choice_text": "Agiliq"
          }
        ]
      }
    ]
  }
}

源码:  Github – graphql_test_server

发表评论