Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Schema first approach to GraphQL server development in Go

Schema first approach to GraphQL server development in Go

Youichi Fujimoto

October 28, 2019
Tweet

More Decks by Youichi Fujimoto

Other Decks in Technology

Transcript

  1. Schema first approach to GraphQL
    server development in Go
    Youichi FUJIMOTO
    MonotaRO Co.,Ltd.

    View full-size slide

  2. About Me
    Youichi FUJIMOTO
    MonotaRO データマーケティング部
    EC基盤グループ
    https://medium.com/@yofujimo
    https://github.com/fpt
    https://twitter.com/_fp
    Go 歴は半年くらい
    Go modulesになって再入門
    これの続きの話をします。

    View full-size slide

  3. About GraphQL
    APIのためのクエリ言語であり、ユーザー定義した型システムによるクエリを実行するた
    めのサーバーサイドランタイム。データベースやストレージには依存せず、既存のコード
    やデータを利用できる。
    ● Query
    ● Mutation
    ● Subscription
    Goのライブラリ:
    graphql-go, graph-gophers,
    gqlgen, thunder etc.
    今回使うのは、
    https://github.com/graph-gophers/graphql-go
    AWS AppSync

    View full-size slide

  4. Model
    Resolver
    処理フローに見るSchemaとResolverとModelの関係
    Queryのリクエストは
    QueryResolver -> DataSource -> Model -> Resolver -> Response
    のように処理される。
    したがって、(Query)Resolver と Model は Schema と DataSource(とGo)をつなぐ役
    割を担う。
    https://graphql.org/learn/execution/
    Resolver Model
    Resolver Model
    DataSource
    parse
    validate
    QueryResolver
    Schema

    View full-size slide

  5. GraphQL Schemaの型とModelの型は以下のような関係になる。
    Resolverが扱う型の対応関係
    GraphQL Type Model (Go) DataSource (Ex. SQL)
    Int! int32 INT
    Float! float64 DOUBLE
    String! string VARCHAR(N)
    Boolean! boolean BOOLEAN
    Time! (user defined) time.Time TIMESTAMP
    ID! graphql.ID VARCHAR(N)
    Int64はGraphQL specには含まれておらず IDとすることが多い。
    その場合ResolverでID <-> int64変換が発生
    Nullableはポインタ
    *int32, *string, etc.

    View full-size slide

  6. 一般的なGraphQLサーバーの構成要素 (1/2)
    type Dog {
    name: String!
    age: Int!
    }
    type Query {
    dog(name: String!): Dog
    }
    type DogResolver struct {
    m *Dog
    }
    func (r *DogResolver) Name() string {
    return r.m.Name
    }
    func (r *DogResolver) Age() int32 {
    return int32(r.m.Age)
    }
    type Dog struct {
    Name string
    Age int
    }
    Schema Resolver Model
    この辺がつらい!

    View full-size slide

  7. 一般的なGraphQLサーバーの構成要素 (2/2)
    func (q *query) Dog(ctx context.Context, args struct{ Name string }) *DogResolver {
    r, _ := tbl.ReadRow(ctx, args.Name)
    dog := model.Dog{
    Name: string(row[“dogs”][0].Value),
    Age: strconv.Atoi(string(row[“dogs”][1].Value)),
    }
    return &DogResolver{m: *dog}
    }
    QueryResolver
    この辺もつらい!

    View full-size slide

  8. 今回やりたいこと
    テーブルごと、カラムごとに全部書くのは大変!
    ⇒ Schema から Resolver と Model をコード生成して
      KVS (今回はBigtable) を参照したい
    go generate
    Resolver Model Bigtable
    reflect
    Schema First
    parse
    validate
    Schema
    tag
    型の
    SSOT

    View full-size slide

  9. GraphQL Schemaに情報を付加する
    今回は Descriptions を使ってテーブルのカラム情報を追加してみた。
    GraphQL spec June 2018 release で Block string も使えるように。
    type Dog {
    "dog:name"
    name: String!
    "dog:age"
    age: Int!
    }
    name age
    Pochi 3
    Shiro 5
    Table: dog

    View full-size slide

  10. Go generateとは
    コード生成のツールを実行するための go のコマンド。
    ソースコードに埋め込まれた下記のようなコメントからコマンドを実行する。
    build とは異なるため依存関係まではチェックされない。
    //go:generate goyacc -o gopher.go -p parser gopher.y
    "go/format” を使うことで、 go fmt した状態のソースを生成できる。
    任意のコマンドを実行できるので、
    今回右のような構成で実行した。 gen_gql
    my_gql //go:generate ../gen_gql/main ...
    project
    https://blog.golang.org/generate
    ↑冒頭がカッコイイので必読!

    View full-size slide

  11. 今回は "gqlparse” を使う。(gqlgen で使われているものと同じ)
    https://github.com/vektah/gqlparser
    Go generate (Model)
    type Dog {
    "dog:name"
    name: String!
    "dog:age"
    age: Int!
    }
    入力
    出力
    type Dog struct {
    Name string `dog:"name"`
    Age int `dog:"age"`
    }

    View full-size slide

  12. Go generate (Model)
    import (
    "github.com/vektah/gqlparser"
    gqlast "github.com/vektah/gqlparser/ast"
    )
    s, err := gqlparser.LoadSchema(&gqlast.Source{Name: filename, Input: schema})
    for _, typeName := range types {
    g.generateModelHead(typeName)
    for _, field := range s.Types[typeName].Fields {
    g.generateModelField(typeName, field.Name, field.Type.String(),
    field.Description)
    }
    g.generateModelTail()
    }

    View full-size slide

  13. Go generate (Model)
    var gqlToGoType = map[string]string{
    "ID!": "string",
    "ID": "*string",
    "Int!": "int",
    "Int": "*int",
    "String!": "string",
    "String": "*string",
    "Boolean!": "bool",
    "Boolean": "*bool",
    }
    GraphQLの型とGoの型との対応関係を記述する。

    View full-size slide

  14. Go generate (Model)
    func (g *Generator) generateModelField(
    structName,
    fieldName,
    fieldType,
    desc string,
    ) {
    fieldNamePascal := strings.Title(fieldName)
    tblcol := strings.Split(desc, ":")
    goType, _ := gqlToGoType[fieldType]
    g.Printf("%s %s `%s:\"%s\"`\n", fieldNamePascal, goType, tblcol[0],
    tblcol[1])
    }
    Schemaの型をGoの型に変換しつつ、Modelの各フィールドを生成する。

    View full-size slide

  15. Go generate (Resolver)
    func (r *DogResolver) Name() string { return r.m.Name }
    func (r *DogResolver) Age() int32 { return int32(r.m.Age) }
    type DogResolver struct {
    m *model.Dog
    }
    Resolver本体は
    事前に定義する
    Resolver自体のフィールドや、データの
    取得はテーブル設計依存なので自動生
    成しない
    フィールドだけ生成する
    type Dog {
    name: String!
    age: Int!
    }
    入力
    出力

    View full-size slide

  16. Go generate (Resolver)
    func GenerateResolver(g *Generator, types []string, s *gqlast.Schema)
    {
    for _, typeName := range types {
    for _, field := range s.Types[typeName].Fields {
    g.generateResolverField(typeName, strings.Title(field.Name),
    field.Type.String())
    }
    }
    src := g.format()
    }
    Schemaのtypeごとに、フィールドを取得する。

    View full-size slide

  17. Go generate (Resolver)
    func (g *Generator) generateResolverField(structName, fieldName, fieldType
    string) {
    switch fieldType {
    case "Int!":
    g.Printf("func (r *%sResolver) %s() int32 { return int32(r.m.%s) }",
    structName, fieldName, fieldName)
    case "String!":
    g.Printf("func (r *%sResolver) %s() string { return r.m.%s }", structName,
    fieldName, fieldName)

    }
    }
    フィールドに対応するResolverのメソッドを生成する。
    型変換が発生するので、あえて mapは
    使わずswitchで。

    View full-size slide

  18. データの流れ
    go generate を使って Schema から Resolver と Model のコードを生成できた
    でも QueryResolver で Model ごとに Row から読み込むのは大変・・
    ⇒ reflect で解決!
    KVS
    Resolver Model
    reflect
    parse
    validate
    Schema
    go generate
    tag

    View full-size slide

  19. reflectとは
    go のreflect はさまざまな型のオブジェクトを扱うことのできる、実行時のリフレクションを
    提供する。典型的な使い方では TypeOf で静的な型から動的な型情報である Type を
    取得する。struct の tag も参照可能。
    また、Type から新たに New して interface{} として取得することもできる。
    簡略化のため今回は "github.com/mitchellh/mapstructure" も使う
    t := reflect.TypeOf(x)
    m := reflect.New(t).Interface()
    y := reflect.ValueOf(m).Elem().Interface()

    View full-size slide

  20. Row から Model へ
    Model の tag からカラム名を抽出し、Row との対応関係を取得
    そして Row の各フィールドの Type を取得する
    func LoadModel(m interface{}, row bigtable.Row) (interface{}, error) {
    tp := reflect.TypeOf(m)
    for i := 0; i < tp.NumField(); i++ {
    typ := tp.Field(i)
    tag := strings.ReplaceAll(string(typ.Tag), "\"", "")
    fieldMap[tag] = typ
    }
    ...
    for _, itm := range row[cf] {
    col := itm.Column
    typ, ok := fieldMap[col]
    ...

    View full-size slide

  21. Row から Model へ
    フィールドごとに Model の型へ変換して一旦 map[string]interface{} に詰め直す。そし
    て mapstructure で Model に入れる。
    mapstructure.Decode(data, m)
    switch t := typ.Type.Kind(); t {
    case reflect.String:
    data[typ.Name] = string(itm.Value)
    case reflect.Int:
    data[typ.Name], _ = strconv.Atoi(string(itm.Value))

    }

    View full-size slide

  22. Row から Model へ
    あとは QueryResolver を書けばおしまい。
    func (q *query) Dog(ctx context.Context, args struct{Name string}) *DogResolver {
    dog := model.LoadModel(ctx, &model.Dog{}, args.Name)
    return &DogResolver{
    m: dog,
    }
    }

    View full-size slide

  23. 最終的に記述するもの
    type Dog {
    "dog:name"
    name: String!
    "dog:age"
    age: Int!
    }
    type Query {
    dog(name: String!): Dog
    }
    type DogResolver struct {
    m *Dog
    }
    Schema Resolver
    func (q *query) Dog(ctx context.Context, args struct{Name
    string}) *DogResolver {
    dog := model.LoadModel(ctx, &model.Dog{}, args.Name)
    return &DogResolver{
    m: dog,
    }
    }
    QueryResolver
    型はSchemaに集約された!カラム追加はスキーマだけでOK!

    View full-size slide

  24. まとめ
    GraphQL Schemaに妥当な方法で情報を追加し、go generate で tag を埋め込み
    reflect で Bigtable から Model に読み込むロジックを共通化した。
    ⇨ Goのツール・モジュールを活用することで Schema first が実現できた。
      対象を絞ってアプリ固有ロジックとの住み分けを見極めることが重要。
    QueryResolver
    go generate
    Bigtable
    reflect
    Resolver Model
    parse
    validate
    Schema
    tag
    型の
    SSOT

    View full-size slide

  25. https://www.youtube.com/watch?v=--YArqOTgsk
    We are hiring at Tokyo office!
    For more information,

    View full-size slide