本文档结合了 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
工具可以依据你的数据结构也就是上面的文件graph/schema.graphqls
生成几乎除了逻辑外的大多数代码,设置包括了使用net/http
库启动的服务端。
- 首先需要获得
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
- 进入到项目的目录下,初始化go项目的模块
shell$ cd go-orders-graph-api
shell$ go mod init github.com/yangwawa0323/go-orders-graph-api
- 接着初始化形成
shell$ go run github.com/99designs/gqlgen init
或者直接使用编译后的命令
shell$ gqlgen init
- 这样就形成下面的文件结构
├── 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启动服务
- 默认
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:OrderID
到Order
下的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{})
}
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
删除订单的逻辑都没有实现,因此这是整个程序编写中除了模型定义,初始化数据库连接外,我们需要自主实现的代码编写部分
- 创建订单逻辑,新建一个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
}
- 更新订单,按照订单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
}
- 删除订单,从数据库中按订单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
}
使用 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 目录,目录中有着
mutation
和query
的单独定义,以及types
目录下单个结构的定义
遇到这种情况,我们需要通过 gqlgen.yaml 的合理配置方可以通过代码生成程序
下面是 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 段的格式(不适用于分离模式)
# 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下载查看
可以让 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 结合使用