GoでRDB（gorm）とGraphQLを繋げてみた話

# はじめに
前回【[RDBでGraphQL使いたい](https://qiita.com/lightstaff/items/7e7d5479a13469ab625a)】はNodeでやってみましたが、Goでもと思いついたわけです。
面倒なとこが結構あったので残しておくことにしました。

**※クエリのみの対応です（更新は普通にORM使った方がいいです）**

## 構成

- golang v1.11
- gorm v1.9.2
- graphql-go v0.7.7

## 面倒になった主な理由
- 独自型を突っ込んだ。
- あえて循環したクエリの結果が欲しかった（[成果物](##成果物)の画像のような）。

## 成果物
![image.png](https://qiita-image-store.s3.amazonaws.com/0/94252/e8342a54-28e2-2003-75a5-fe2625dd77f6.png)

https://github.com/lightstaff/go-graphql-gorm-example

# GORM DAO定義
早速gormで扱うDAOを定義します。

●ユーザー
「メール」を子として持ちます。

```golang:dao.user.go
package dao

// DAO上のユーザー
type User struct {
	ID     int64 `gorm:"PRIMARY_KEY"`
	Name   string
	EMails []EMail
}
```

●メール
「ユーザー」を親として持ちます。

```golang:dao.email.go
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のインターフェースを持たせただけの拡張です。

```golang:types.null_string.go
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の標準型から外れるので解決方法を定義する必要があります。

```golang:scalar.null_string_scalar.go
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`を定義します。

```golang:main.go
// 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()`）です。

```golang:main.go
// 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らしい。