尝试将 Go 中的 RDB(gorm)和 GraphQL 进行连接的故事
首先
我之前想在Node中使用GraphQL,但后来我决定在Go中尝试一下。
因为有一些麻烦事情,所以我决定将其保留下来。
※仅支持查询操作(建议使用常规ORM进行更新操作)。
形成
-
- golang v1.11
-
- gorm v1.9.2
- graphql-go v0.7.7
导致感到麻烦的主要原因是什么?
-
- 独自型を突っ込んだ。
- あえて循環したクエリの結果が欲しかった(成果物の画像のような)。
达到的结果/成果
gorm dao 是一个用于定义和访问数据库的框架。
我们首先定义在gorm中处理的DAO。
●用户有一个名为“邮件”的子项。
package dao
// DAO上のユーザー
type User struct {
ID int64 `gorm:"PRIMARY_KEY"`
Name string
EMails []EMail
}
●邮件以“用户”为父对象存在。
package dao
import "github.com/lightstaff/go-graphql-gorm-example/dao/types"
// DAO上のメール
type EMail struct {
ID int64 `gorm:"PRIMARY_KEY"`
Address string
Remarks types.NullString
UserID int64
User *User // 循環参照できるようにポインタで定義
}
※types.NullString是对sql.NullString进行了JSON接口的扩展而已。
package types
import (
"database/sql"
"encoding/json"
)
// sql.NullStringのラッパー
type NullString struct {
sql.NullString
}
// MarshalJSON
func (s NullString) MarshalJSON() ([]byte, error) {
if s.Valid {
return json.Marshal(s.String)
}
return json.Marshal(nil)
}
// UnmarshalJSON
func (s *NullString) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
s.String = str
s.Valid = str != ""
return nil
}
// 新規作成
func NewNullString(value string) NullString {
return NullString{
NullString: sql.NullString{
String: value,
Valid: value != "",
},
}
}
这是一个常见的gorm定义。
GraphQL Scalar的定义
这是一个繁琐的问题。因为NullString不符合GraphQL的标准类型,所以需要定义解决方法。
package scalar
import (
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast"
"github.com/lightstaff/go-graphql-gorm-example/dao/types"
)
// NullStringの変換定義
var NullStringScalar = graphql.NewScalar(graphql.ScalarConfig{
Name: "NullString",
Description: "Support for null string",
Serialize: func(value interface{}) interface{} {
switch value := value.(type) {
case types.NullString:
return value.String
case *types.NullString:
return value.String
default:
return nil
}
},
ParseValue: func(value interface{}) interface{} {
switch value := value.(type) {
case string:
return types.NewNullString(value)
case *string:
return types.NewNullString(*value)
default:
return nil
}
},
ParseLiteral: func(valueAST ast.Value) interface{} {
switch valueAST := valueAST.(type) {
case *ast.StringValue:
return types.NewNullString(valueAST.Value)
default:
return nil
}
},
})
根据 graphql.ScalarConfig 的要求,我们定义了可以相互转换的 Serialize 和 ParseValue 方法。关于 ParseLiteral 的理解还有些困惑……
这次只有空字符串,但是如果独自类型增加的话可能会很麻烦…
GraphQL Object定义的目的是描述GraphQL的数据模型,包括字段和类型。它使用了一种类似于JSON的语法,用于定义对象的结构和字段的类型。GraphQL Object定义也可以包含嵌套对象和自定义类型,从而构建复杂的数据模型。
以下是另一个麻烦的点。
当然,gorm查询的结果并不会自动转换成GraphQL的模式。您需要定义graphql.Object。
// graphql.Object定義部分のみ抜粋
// GraphQL上のユーザー定義
var userType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.User); ok {
return data.ID, nil
}
return nil, nil
},
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.User); ok {
return data.Name, nil
}
return nil, nil
},
},
},
})
// GraphQL上のメール定義
var emailType = graphql.NewObject(graphql.ObjectConfig{
Name: "EMail",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.EMail); ok {
return data.ID, nil
}
return nil, nil
},
},
"address": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.EMail); ok {
return data.Address, nil
}
return nil, nil
},
},
"remarks": &graphql.Field{
Type: scalar.NullStringScalar,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.EMail); ok {
return data.Remarks, nil
}
return nil, nil
},
},
"userId": &graphql.Field{
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.EMail); ok {
return data.UserID, nil
}
return nil, nil
},
},
},
})
// GraphQL循環参照エラー対策
func init() {
userType.AddFieldConfig("emails", &graphql.Field{
Type: graphql.NewList(emailType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.User); ok {
return data.EMails, nil
}
return nil, nil
},
})
emailType.AddFieldConfig("user", &graphql.Field{
Type: userType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if data, ok := p.Source.(dao.EMail); ok {
if data.User != nil {
return *data.User, nil
}
return nil, nil
}
return nil, nil
},
})
}
在init()函数中正在进行一些操作,但是GraphQL库提供了一种友好的设计策略,用graphql.NewObject来定义用户的邮件属性为数组,并且用邮件属性来定义用户,这样可以避免循环引用导致的编译错误。只需按照这种方式进行后期添加即可通过编译。
程序入口
只要到达这里,剩下的就是入口点(func main())了。
// mainのみ抜粋
func main() {
db, err := gorm.Open("mysql", "root:1234@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(
graphql.ObjectConfig{
Name: "query",
Fields: graphql.Fields{
"users": &graphql.Field{
Type: graphql.NewList(userType),
Description: "Users",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
users := make([]dao.User, 0)
if err := db.Preload("EMails").Preload("EMails.User").Find(&users).Error; err != nil {
return nil, err
}
return users, nil
},
},
},
},
),
})
if err != nil {
panic(err)
}
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})
http.Handle("/graphql", h)
http.ListenAndServe(":8080", nil)
}
当你运行 `go run main.go` 并访问 `localhost:8080/graphql` 时,GraphiQL将打开。你可以执行像#开始的##成果物中贴的图片一样的查询。
总之
可以说,各种简单的转换都是Go风格所需要的。