はじめに
前回【RDBでGraphQL使いたい】はNodeでやってみましたが、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
はよく分からん・・・。
今回はNullStirng
だけでしたが、独自型が増えるとしんどいかも・・・。
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らしいといえばGoらしい。