Skip to content

yangwawa0323/gqlgen-todo3

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL 和 GORM

本文档结合了 GraphQL 和 GORM 库,目的实现基于 MySQL 数据库的 WEB API 框架。

首先新建一个空目录,存放我们的项目

mkdir go-orders-graph-api/graph -pv
cd go-orders-graph-api

定义数据结构

首先定义一个数据库所用的数据结构。创建一个文件**graph/schema.graphqls**

type Order {
    id: Int!
    customerName: String!
    orderAmount: Float!
    items: [Item!]!
}

type Item {
    id: Int!
    productCode: String!
    productName: String!
    quantity: Int!
}

input OrderInput {
    customerName: String!
    orderAmount: Float!
    items: [ItemInput!]!
}

input ItemInput {
    productCode: String!
    productName: String!
    quantity: Int!
}

type Mutation {
    createOrder(input: OrderInput!): Order!
    updateOrder(orderId: Int!, input: OrderInput!): Order!
    deleteOrder(orderId: Int!): Boolean!
}

type Query {
    orders: [Order!]!
}

gqlgen 工具

使用 gqlgen 工具可以依据你的数据结构也就是上面的文件graph/schema.graphqls生成几乎除了逻辑外的大多数代码,设置包括了使用net/http库启动的服务端。

  1. 首先需要获得gqlgen以及数据库开发所需的gorm工具库
shell$ go get -u gorm.io/gorm
shell$ go get -u github.com/go-sql-driver/mysql
shell$ go get github.com/99designs/gqlgen
  1. 进入到项目的目录下,初始化go项目的模块
shell$ cd go-orders-graph-api
shell$ go mod init github.com/yangwawa0323/go-orders-graph-api
  1. 接着初始化形成
shell$ go run github.com/99designs/gqlgen init

或者直接使用编译后的命令

shell$ gqlgen init
  1. 这样就形成下面的文件结构
├── go.mod
├── go.sum
├── gqlgen.yml               - gqlgen 配置文件,用来控制生成的代码.
├── graph
│   ├── generated            - 仅仅包含生成的generated包
│   │   └── generated.go
│   ├── model                - 生成的所有数据模型
│   │   └── models_gen.go
│   ├── resolver.go          - 此文件不会因为重新生成而覆盖
│   ├── schema.graphqls      - 数据结构
│   └── schema.resolvers.go  - 自己定义的如何实现 schema.graphql逻辑功能
└── server.go                - Web启动服务
  

graph/schema.graphqls结构文件

  1. 默认qglgen将会自动生成一个Todo的案例,我们需要删除以前的内容
shell$ rm ./graph/generated/* ./graph/model/* 

然后可以开始重新定义数据结构,清空之前的Todo结构,我们将建立一个订单的范例

shell$ vim graph/schema.graphqls

type Order {
    id: Int!
    customerName: String!
    orderAmount: Float!
    items: [Item!]!
}

type Item {
    id: Int!
    productCode: String!
    productName: String!
    quantity: Int!
}

input OrderInput {
    customerName: String!
    orderAmount: Float!
    items: [ItemInput!]!
}

input ItemInput {
    productCode: String!
    productName: String!
    quantity: Int!
}

type Mutation {
    createOrder(input: OrderInput!): Order!
    updateOrder(orderId: Int!, input: OrderInput!): Order!
    deleteOrder(orderId: Int!): Boolean!
}

type Query {
    orders: [Order!]!
}

模型的定义

一旦修改好,再次生成我们的代码

gqlgen generate

这样我们再次得到之前的目录结构中的每个文件。在整个交互中,我们只需保留schema.graphqls文件即可

生成的模型graph/models/models_gen.go如下

package model
//...
type Item struct {
	ID          int `json:"id"`
	ProductCode string `json:"productCode"`
	ProductName string `json:"productName"`
	Quantity    int    `json:"quantity"`
}

type Order struct {
	ID           int  `json:"id"`
	CustomerName string  `json:"customerName"`
	OrderAmount  float64 `json:"orderAmount"`
	Items        []*Item `json:"items"`
}

为了实现GORM数据结构,我们可以添加关于外键的定义,按照需求,一个订单有多条记录,添加注解gorm:foreignkey:OrderIDOrder下的Items,这将以为着Item表中的OrderID列外键形式参考的为Order表的ID,详细请参考GORM

package model
//...
type Order struct {
	ID           int  `json:"id"`
	CustomerName string  `json:"customerName"`
	OrderAmount  float64 `json:"orderAmount"`
    Items        []*Item `json:"items" gorm:"foreignkey:ID"`
}

最终,如下

package model

type ItemInput struct {
	ProductCode string `json:"productCode"`
	ProductName string `json:"productName"`
	Quantity    int    `json:"quantity"`
}

type OrderInput struct {
	CustomerName string       `json:"customerName"`
	OrderAmount  float64      `json:"orderAmount"`
	Items        []*ItemInput `json:"items"`
}

type Item struct {
	ID          int `json:"id"`
	ProductCode string `json:"productCode"`
	ProductName string `json:"productName"`
	Quantity    int    `json:"quantity"`
    OrderID		uint	`json:"-"`
}

type Order struct {
	ID           int  `json:"id"`
	CustomerName string  `json:"customerName"`
	OrderAmount  float64 `json:"orderAmount"`
    Items        []*Item `json:"items" gorm:"foreignkey:OrderID"`
}

特别注意,GORM反映外键关系,首先需要在从表中添加一列,上面的例子中Items结构体中我们添加了OrderID 新属性而注解 **json:"-"**代表在查询中此属性将被JSON序列化时忽略,而在 Order中关于 Items属性的注解中我们额外添加了 gorm:"foreignkey:OrderID,这将告知GORM转化成数据库的表形态时,一对多关系中,order 表中的主键将成为 items 表中的外键参考,更多请参考官方GORM has-many

导入开发库

思路:首先我们需要用到数据库,因此我们在以下文件中导入我们开发所用的 gorm

// In resolver.go
import "gorm.io/gorm"

注意:案例中代码使用到的非grom.io/grom,而是github.com下的。请保持一致,混杂使用两种库,一定会出现看上去切片指针一样,而在使用中总是报错的现象

还有一点,如果你使用vscode编写代码golint会在保存时自动去除没有使用但导入的库,你所做的操作就会白费。

// In schema.resolver.go
import (
	"context"
	"github.com/yangwawa0323/go-orders-graphql-api/graph/generated"
	"github.com/yangwawa0323/go-orders-graphql-api/graph/model"
)
// In server.go
import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	_ "github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
	"github.com/soberkoder/go-orders-graphql-api/graph"
	"github.com/soberkoder/go-orders-graphql-api/graph/generated"
	"github.com/soberkoder/go-orders-graphql-api/graph/model"
)

注意:引入模块的名称是基于go mod init建立项目时的名称。如果你是复制以上代码,请自行修正

初始化数据库

为了将 Graphql 和 GORM结合,我们可以在项目根目录下的服务启动程序server.go中添加初始化数据库代码

# In server.go
var db *gorm.DB;

func initDB() {
    var err error
    dataSourceName := "root:redhat@tcp(localhost:3306)/?parseTime=True&charset=utf8mb4"
    db, err = gorm.Open("mysql", dataSourceName)

    if err != nil {
        fmt.Println(err)
        panic("failed to connect database")
    }

    db.LogMode(true)

    // Create the database. This is a one-time step.
    // Comment out if running multiple times - You may see an error otherwise
    db.Exec("CREATE DATABASE IF NOT EXISTS test_db")
    db.Exec("USE test_db")

    // Migration to create tables for Order and Item schema
    db.AutoMigrate(&models.Order{}, &models.Item{})	
}

GORM 数据库连接

resolver.go 文件中添加初始化数据库db的指针

# In resolver.go
type Resolver struct{
    DB *gorm.DB
}

在服务器中调用先前的 initDB 函数, 并在 Resolver 初始化的传递已经建立连接的 DB 对象

# In server.go
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}
	initDB()
	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{
		DB: db,
	}}))
	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
	http.Handle("/query", srv)
	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

双向数据操作

之前在模型定义中的createOrder创建订单、updateOrder更新订单、deleteOrder删除订单的逻辑都没有实现,因此这是整个程序编写中除了模型定义,初始化数据库连接外,我们需要自主实现的代码编写部分

  1. 创建订单逻辑,新建一个Order对象,使用DB.Create保存到数据库,再将订单数据返回,交由generated库序列化转换成JSON格式返回给客户端
func (r *mutationResolver) CreateOrder(ctx context.Context, input OrderInput) (*models.Order, error) {
    order := models.Order {
        CustomerName: input.CustomerName,
        OrderAmount: input.OrderAmount,
        Items: mapItemsFromInput(input.Items),
    }
    r.DB.Create(&order)
    return &order, nil
}
  1. 更新订单,按照订单ID号,以及数据建立新的对象,使用DB.Save保存到数据库,同样返回给generated库序列化成JSON结构返回给客户
func (r *mutationResolver) UpdateOrder(ctx context.Context, orderID int, input OrderInput) (*models.Order, error) {
    updatedOrder := models.Order {
        ID: orderID,
        CustomerName: input.CustomerName,
        OrderAmount: input.OrderAmount,
        Items: mapItemsFromInput(input.Items),
    }
    r.DB.Save(&updatedOrder)
    return &updatedOrder, nil
}
  1. 删除订单,从数据库中按订单ID找出订单让后使用 DB.Delete 删除从表和主表的记录。
func (r *mutationResolver) DeleteOrder(ctx context.Context, orderID int) (bool, error) {
    r.DB.Where("id = ?", orderID).Delete(&models.Item{})
    r.DB.Where("id = ?", orderID).Delete(&models.Order{})
    return true, nil;
}

查询操作

func (r *queryResolver) Orders(ctx context.Context) ([]*models.Order, error) {	
    var orders []*models.Order
    r.DB.Preload("Items").Find(&orders)
    
    return orders, nil
}

运行服务

go run server.go

打开浏览器访问 http://localhost:8080

测试

  • 创建订单
mutation createOrder ($input: OrderInput!) {
  createOrder(input: $input) {
    id
    customerName
    items {
      id
      productCode
      productName
      quantity
    }
  }
}

传入的参数

{
  "input": {
    "customerName": "Leo",
    "orderAmount": 9.99,
    "items": [
      {
      "productCode": "2323",
      "productName": "IPhone X",
      "quantity": 1
      }
    ]
  }
}
  • 查询订单
  query orders {
    orders {
      id  
      customerName
      items {
        productName
        quantity
      }
    }
  }
  • 更新订单
  mutation updateOrder ($orderId: Int!, $input: OrderInput!) {
    updateOrder(orderId: $orderId, input: $input) {
      id
      customerName
      items {
        id
        productCode
        productName
        quantity
      }
    }
  }

传入参数

  {
    "orderId":1,
    "input": {
      "customerName": "Cristiano",
      "orderAmount": 9.99,
      "items": [
        {
        "productCode": "2323",
        "productName": "IPhone X",
        "quantity": 1
        }
      ]
    }
  }
  • 删除订单
mutation deleteOrder ($orderId: Int!) {
    deleteOrder(orderId: $orderId)
}

传入参数

{
  "orderId": 3
}

Javascript客户端的访问

使用 GrapheQL 最大的好处体现在客户端的调用

        var customerName = 'yangkun'
        var productCode = '2023'
        var quantity = 8.0, orderAmount = 9.9
        var productName = 'IBM x61 notebook pc'
        var input = {
            customerName,
            orderAmount,
            "items": [
                {
                    productCode,
                    productName,
                    quantity
                }
            ]
        }
        var query = `mutation createOrder ($input: OrderInput!) {
  createOrder(input: $input) {
    customerName
    items {
      productCode
      productName
      quantity
    }
  }
}`

        fetch('/query', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query,
                variables: { input },
            })
        })



        fetch('/query', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query: `query orders {
    orders {
      id  
      customerName
      items {
        productName
        quantity
      }
    }
  }`
            })
        })
            .then(r => r.json())
            .then(data => console.log('data returned:', data));

将之前的 queru 或者 mutation 语句通过 POST 方式发送JSON数据给统一的后端地址,并且通过 promise 函数格式取回结果.a

分离模型的定义

在有些时候我们需要将 graphql 文件分开定义. 比如说: user.graphql, todo.graphql

下面是一个常见的分离目录结构

> shell# tree
.
├── go.mod
├── go.sum
├── gqlgen.yml
├── gqlgen.yml.org
├── graph
│   ├ ── generated
│   │   └── generated.go
│   └── model
│       └── models_gen.go
├── resolvers
│   └── resolver.go
├── schema
│   ├── mutation.graphql
│   ├── query.graphql
│   └── types
│       ├── todo.graphql
│       └── user.graphql
└── server.go

6 directories, 12 files

上面的例子中,存在单独的 schema 目录,目录中有着mutationquery的单独定义,以及types目录下单个结构的定义

遇到这种情况,我们需要通过 gqlgen.yaml 的合理配置方可以通过代码生成程序

gqlgen.yml 配置

下面是 gqlgen.yml 的详细配置和说明

# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.

schema:
  - "schema/**/*.graphql"

exec:
  filename: graph/generated/generated.go
  package: generated

model:
  filename: graph/model/models_gen.go
  package: model

resolver:
  filename: resolvers/resolver.go
  package: resolvers
  type: Resolver

# Optional, set to true if you prefer []Thing over []*Thing
omit_slice_element_pointers: true

autobind: []

重点在于 resolver 段的配置, 由于单独分离了schema 的定义,我们将不能使用缺省的 layout:follow-schema 的方式,这样会造成不同的目录下的包名称都 graph 而产生 golang 包引入的错误

更多解决方法,请继续查看最后的解决方案

分离之前的resolver段

下面为原始的resolver 段的格式(不适用于分离模式)

# Where should the resolver implementations go
resolver:
  layout: follow-schema
  dir: graph
  package: graph

# Optional: turn on use `gqlgen:"fieldName"` tags in your models
# struct_tag: json

正确的格式应该为

resolver:
  filename: resolvers/resolver.go
  package: resolvers
  type: Resolver

上面这段的意思是将所有的 schema 最终生成到一个 resolvers/resolver.go文件中.

范例可以从 github.com/yangwawa0323/gqlgen-todo3下载查看

model 段的分离

可以让 gqlgen 生成的模块于 autobind 生成的模块放入统一的包名中

# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.

schema:
  - "schema/**/*.graphql"

exec:
  filename: graph/generated/generated.go
  package: generated

model:
  #filename: graph/model/models_gen.go
  ###################################################
  # Put all models in the folder <project_dir>/models
  filename: models/models_gen.go
  package: models

resolver:
  filename: resolvers/resolver.go
  package: resolvers
  type: Resolver

# Optional, set to true if you prefer []Thing over []*Thing
omit_slice_element_pointers: true

###############################################################
# autobind the models which were been defined in golang struct 
# referencing in the gqlgen gernereate schemas.
autobind: 
  - "github.com/yangwawa0323/gqlgen-todo3/models"
#[]

注意:每次在添加新的schema,重新运行 gqlgen generate的时候,需要先删除原先的 resolvers/resolver.go 文件, 注意备份原有的 resolver 中的信息.

resolver段也可以根据文件做分离, 下面的例子给出了详细的配置

resolver:
  # filename: resolvers/resolver.go
  layout: follow-schema
  dir: resolvers
  package: resolvers
  type: Resolver
  filename_template: "{name}.resolvers.go

dir : 定义目录夹

layout: follow-schema 需要和 filename_template 结合使用

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages