Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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.

Slide 6

Slide 6 text

一般的な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 この辺がつらい!

Slide 7

Slide 7 text

一般的な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 この辺もつらい!

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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 ↑冒頭がカッコイイので必読!

Slide 11

Slide 11 text

今回は "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"` }

Slide 12

Slide 12 text

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() }

Slide 13

Slide 13 text

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の型との対応関係を記述する。

Slide 14

Slide 14 text

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の各フィールドを生成する。

Slide 15

Slide 15 text

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! } 入力 出力

Slide 16

Slide 16 text

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ごとに、フィールドを取得する。

Slide 17

Slide 17 text

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で。

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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()

Slide 20

Slide 20 text

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] ...

Slide 21

Slide 21 text

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)) … }

Slide 22

Slide 22 text

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, } }

Slide 23

Slide 23 text

最終的に記述するもの 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!

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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