尝试将 Go 中的 RDB(gorm)和 GraphQL 进行连接的故事

首先

我之前想在Node中使用GraphQL,但后来我决定在Go中尝试一下。
因为有一些麻烦事情,所以我决定将其保留下来。

※仅支持查询操作(建议使用常规ORM进行更新操作)。

形成

    • golang v1.11

 

    • gorm v1.9.2

 

    graphql-go v0.7.7

导致感到麻烦的主要原因是什么?

    • 独自型を突っ込んだ。

 

    あえて循環したクエリの結果が欲しかった(成果物の画像のような)。

达到的结果/成果

image.png

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风格所需要的。

广告
将在 10 秒后关闭
bannerAds