LoginSignup
4
0

More than 5 years have passed since last update.

GoでRDB(gorm)とGraphQLを繋げてみた話

Last updated at Posted at 2019-01-17

はじめに

前回【RDBでGraphQL使いたい】はNodeでやってみましたが、Goでもと思いついたわけです。
面倒なとこが結構あったので残しておくことにしました。

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

構成

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

面倒になった主な理由

  • 独自型を突っ込んだ。
  • あえて循環したクエリの結果が欲しかった(成果物の画像のような)。

成果物

image.png

GORM DAO定義

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

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

dao.user.go
package dao

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

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

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.NullStringsql.NullStringにJSONのインターフェースを持たせただけの拡張です。

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

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に従い、SerializeParseValueで相互に変換できるように定義しています。ParseLiteralはよく分からん・・・。

今回はNullStirngだけでしたが、独自型が増えるとしんどいかも・・・。

GraphQL Object定義

次なる面倒くさいポイントです。
当然ながらgormのクエリの結果を自動的にGraphQLのスキーマに変換してくれたりはしません。graphql.Objectを定義します。

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())です。

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らしい。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0