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

Ede974a0e9b6a962d210e1fd7836e7d6?s=128

Youichi Fujimoto

October 28, 2019
Tweet

Transcript

  1. Schema first approach to GraphQL server development in Go Youichi

    FUJIMOTO MonotaRO Co.,Ltd.
  2. About Me Youichi FUJIMOTO MonotaRO データマーケティング部 EC基盤グループ https://medium.com/@yofujimo https://github.com/fpt https://twitter.com/_fp

    Go 歴は半年くらい Go modulesになって再入門 これの続きの話をします。
  3. About GraphQL APIのためのクエリ言語であり、ユーザー定義した型システムによるクエリを実行するた めのサーバーサイドランタイム。データベースやストレージには依存せず、既存のコード やデータを利用できる。 • Query • Mutation •

    Subscription Goのライブラリ: graphql-go, graph-gophers, gqlgen, thunder etc. 今回使うのは、 https://github.com/graph-gophers/graphql-go AWS AppSync
  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
  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.
  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 この辺がつらい!
  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 この辺もつらい!
  8. 今回やりたいこと テーブルごと、カラムごとに全部書くのは大変! ⇒ Schema から Resolver と Model をコード生成して  

    KVS (今回はBigtable) を参照したい go generate Resolver Model Bigtable reflect Schema First parse validate Schema tag 型の SSOT
  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
  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 ↑冒頭がカッコイイので必読!
  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"` }
  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() }
  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の型との対応関係を記述する。
  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の各フィールドを生成する。
  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! } 入力 出力
  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ごとに、フィールドを取得する。
  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で。
  18. データの流れ go generate を使って Schema から Resolver と Model のコードを生成できた

    でも QueryResolver で Model ごとに Row から読み込むのは大変・・ ⇒ reflect で解決! KVS Resolver Model reflect parse validate Schema go generate tag
  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()
  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] ...
  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)) … }
  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, } }
  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!
  24. まとめ GraphQL Schemaに妥当な方法で情報を追加し、go generate で tag を埋め込み reflect で Bigtable

    から Model に読み込むロジックを共通化した。 ⇨ Goのツール・モジュールを活用することで Schema first が実現できた。   対象を絞ってアプリ固有ロジックとの住み分けを見極めることが重要。 QueryResolver go generate Bigtable reflect Resolver Model parse validate Schema tag 型の SSOT
  25. https://www.youtube.com/watch?v=--YArqOTgsk We are hiring at Tokyo office! For more information,