GAE GoとDatastoreでGraphQL APIを開発する

Google Cloud Platformのマネージドサービスを使ってGraphQL APIを開発してみました。GraphQLについては初心者でしたので、実装しながらGraphQLについて学んだことを記録します。

## 利用した技術
- __App Engine SE Go 1.11__  
  Go 1.11が11月にβリリース
  2nd genと呼ばれる次世代ランタイム  
- __Cloud Datastore__  
  スケーラビリティの高いNoSQLデータベース
- __Stackdriver__  
  [Stackdriver Trace for Go](https://cloud.google.com/trace/docs/setup/go)を使いたかったため使用

個人で使う分には無料でいけます。動作も速い！！
GAEの2nd genではプラットフォームのことはあまり意識することなく普通にサーバーを実装すればよいです。
にもかかわらず、必要な時、必要な分だけ高速に起動するので最高です。

## 主なライブラリ
使用した主なライブラリは以下のとおりです。
- [googleapis/google-cloud-go](https://github.com/googleapis/google-cloud-go)
  GCPが提供するSDK
- [graphql-go/graphql](https://github.com/graphql-go/graphql)
  Golang向けのGraphQLライブラリ
- [graph-gophers/dataloader](https://github.com/graph-gophers/dataloader)
  リゾルバーを遅延評価してバッチ処理するためのライブラリ（後述）

Go言語用のGraphQLライブラリには大きく2つのアプローチがあるようです。  
- コードからスキーマを生成する（リフレクション）
- スキーマからコードを生成する

両方試してみたいですが、今回試したライブラリは前者です。
ただし、実際試してみて、後者のアプローチのほうが良さそうな印象を持ちました。Goではコードジェネレーションが主流な印象がありますし、どうしても空インターフェースや型アサーションに依存する箇所が多くなりがちだからです。
今度は、後者のアプローチ（[99designs/gqlgen](https://github.com/99designs/gqlgen)と[vektah/dataloaden](https://github.com/vektah/dataloaden)の組み合わせ）も試してみたいと思います。
なお、今回の記事では、GraphQLを理解することとGAEで動かすことを主眼に置いているので、このあたりについては深く言及しません。

## つくってみたもの
コードは[GitHub](https://github.com/monmaru/gae-graphql)で公開しています。  

ブログ投稿APIを題材にしました。題材に特に大きな意味はありません。
Datastoreでは構造体の定義がそのままスキーマになります。
Datastoreには外部キーといった概念はないので、`Blog`構造体に`User`構造体の`ID`フィールドと同じ値である`UserID`フィールドを持たせることで関連を表現しています。
ユーザーが複数のブログを持つという単純な1対多の関係です。

```golang
type User struct {
	ID    string `json:"id" datastore:"-"`
	Name  string `json:"name"`
	EMail string `json:"email"`
}

type Blog struct {
	ID        string    `json:"id" datastore:"-"`
	UserID    string    `json:"userId"`
	CreatedAt time.Time `json:"createdAt"`
	Title     string    `json:"title"`
	Content   string    `json:"content"`
}

type BlogList struct {
	Nodes      []Blog `json:"nodes"`
	TotalCount int    `json:"totalCount"`
}
```

## 開発の流れ
ざっくり以下の手順です。
1. スキーマを定義する
2. リゾルバーを実装する
3. エンドポイント（サーバー）を実装する

それでは、順番に見ていきましょう。

### 1. スキーマを定義する
スキーマはGoの構造体で表現します。
`SchemaConfig`に`Query`や`Mutation`を定義していきます。 
https://github.com/monmaru/gae-graphql/blob/master/application/gql/scheme.go#L9-L13

```golang
func NewSchema(ur repository.UserRepository, br repository.BlogRepository) (graphql.Schema, error) {
	resolver := newResolver(ur, br)
	return graphql.NewSchema(graphql.SchemaConfig{
		Query:    newQuery(resolver),
		Mutation: newMutation(resolver),
	})
}
```

`Mutation`の例です。やることは`Query`の場合でも同じです。
必要な`Field`を定義していきます。
今回は、ユーザーを作成する`createUser`とブログを投稿する`createBlog`です。
`Type`にはAPIで扱うデータを定義し、`Args`にはリクエストから受け取るパラメータを定義します。
`Resolve`には、`Field`を解決するために実行する関数を渡します。（後述）
https://github.com/monmaru/gae-graphql/blob/master/application/gql/mutation.go#L6-L29

```golang
func newMutation(r resolver) *graphql.Object {
	return graphql.NewObject(graphql.ObjectConfig{
		Name: "Mutation",
		Fields: graphql.Fields{
			"createUser": &graphql.Field{
				Type:        newCreateUserInputType(),
				Description: "Add a user",
				Args: graphql.FieldConfigArgument{
					"name":  &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
					"email": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
				},
				Resolve: r.createUsersBatch,
			},
			"createBlog": &graphql.Field{
				Type:        newCreateBlogInputType(),
				Description: "Add a blog",
				Args: graphql.FieldConfigArgument{
					"userId":  &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
					"title":   &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
					"content": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
				},
				Resolve: r.createBlogsBatch,
			},
		},
	})
}
```

### 2. リゾルバーを実装する
単純な例で説明します。
リゾルバーは普通に実装すればよくて`graphql.ResolveParams`からcontextや必要なパラメータを取り出してDatastoreにぶん投げるだけです。
RDBで言うところのINSERT文を実行しています。
このあたりで空インターフェースや型アサーションに頼るところが少々辛いところです。
https://github.com/monmaru/gae-graphql/blob/master/application/gql/resolver.go#L93-L103

```golang
func (r *graphQLResolver) createUser(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
	defer log.Duration(ctx, time.Now(), "[graphQLResolver.createUser]")
	name, _ := params.Args["name"].(string)
	email, _ := params.Args["email"].(string)
	user := &model.User{
		Name:  name,
		EMail: email,
	}
	return r.ur.Create(ctx, user)
}
```

### 3. エンドポイント（サーバー）を実装する
エンドポイントは一つ定義すればよいです。
リクエストボディを全部文字列として読み込んで、`Schema`と一緒に`graphql.Do`に渡します。
最後に、受け取った結果をJSONで返します。
以上！！

```golang
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
    body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: string(body),
    })
    if len(result.Errors) > 0 {
        fmt.Printf("wrong result, unexpected errors: %v", result.Errors)
    }
    json.NewEncoder(w).Encode(result)
})
```

また、[graphql-go/handler](https://github.com/graphql-go/handler)を使うと簡単に`graphiql`や`playground`のWebページを簡単につくれます。
https://github.com/monmaru/gae-graphql/blob/master/interfaces/handler/graphiql.go

## 動きを追ってみよう

2人のユーザー情報とユーザーが投稿した記事を取得してみます。

```golang
{
  u1: user(id: "5635703144710144") {
    name
    posts(limit: 3) {
      nodes {
        title
        createdAt
      }
    }
  }
  u2: user(id: "5153049148391424") {
    name
    posts(limit: 3) {
      nodes {
        title
      }
    }
  }
}
```

無事動きました 🎉🎉

```
{
  "data": {
    "u1": {
      "name": "Jiro",
      "posts": {
        "nodes": [
          {
            "createdAt": "2018-12-16T06:19:15.683822Z",
            "title": "pogeff"
          },
          {
            "createdAt": "2018-12-16T06:19:15.68376Z",
            "title": "mogefff"
          },
          {
            "createdAt": "2018-12-16T06:19:15.683629Z",
            "title": "hogeff"
          }
        ]
      }
    },
    "u2": {
      "name": "Taro",
      "posts": {
        "nodes": []
      }
    }
  }
}
```

さて、内部的にはどのように処理しているのでしょうか 🤔
Stackdriver Traceを使って見てみます。

### Stackdriver Trace
2nd genだと自動で収集してくれないのでちょっと[実装](https://cloud.google.com/trace/docs/setup/go)が必要です。
まずExporterを登録します。

```golang
// Stackdriver Trace
exporter, err := stackdriver.NewExporter(stackdriver.Options{
    ProjectID: projID,
})
if err != nil {
    log.Fatal(err)
}
trace.RegisterExporter(exporter)
```

Propagationを設定してサーバーを起動します。

```golang
server := &http.Server{
    Addr: fmt.Sprintf(":%s", port),
    Handler: &ochttp.Handler{
        Handler:     router,
        Propagation: &propagation.HTTPFormat{},
    },
}
```

こんな感じのコードを追加してデプロイすると、どのように動いているか分かります。
4回ほどDatastoreとRPC通信していて、シーケンシャルに動作していることが分かりました。

<img width="816" alt="slow-trace.png" src="https://qiita-image-store.s3.amazonaws.com/0/16865/aab7ceb3-ee2e-41c1-1db9-926429692850.png">

これは、他の記事でも言及されている`N+1問題`に関係があります。

### N+1問題
GraphQLがリゾルバーを個別にかつ再帰的に実行していくため発生する問題です。
要するに複数のN個のデータをまとめてINSERRTすればいいのに、1個ずつN回のINSERTが実行されるといったことが容易に発生します。

処理時間も長くなってしまいますし、何度も通信してしまうのは避けたいところです。
GraphQLでは遅延評価することでこの問題を解決するようです。

### 遅延評価
N回繰り返しているSQLを1つにまとめる必要があります。
リゾルバーですぐに評価せず、一旦ためて後でまとめて評価することで`N+1問題`を解決します。

FacebookがJavaScript向けの[ライブラリ](https://github.com/facebook/dataloader)を公開していますが、
今回はGo実装である[graph-gophers/dataloader](graph-gophers/dataloader)を使用します。
このライブラリによってDatastoreで`Get`や`Put`ではなく`GetMulti`や`PutMulti`を使って一括処理ができるようになります。

### dataloaderを使ってみる
dataloaderを使うには以下の実装が必要です。

- バッチ処理用の関数を定義
- リゾルバーを実装
- `dataloader.Loader`を生成

#### バッチ処理用の関数を定義

`dataloader.Keys`からパラメータのリストを受け取り、まとめてDatastoreに渡します。
処理結果もリストとして`[]*dataloader.Result`につめて返します。
https://github.com/monmaru/gae-graphql/blob/master/application/gql/batch_functions.go#L23-L51

```golang
return func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
	defer log.Duration(ctx, time.Now(), "[GetUsersBatchFunc]")
	var strIDs []string
	for _, key := range keys {
		strID, ok := key.Raw().(string)
		if !ok {
			return handleError(ctx, errors.New("Invalid key value"))
		}
		strIDs = append(strIDs, strID)
	}

	users, err := ur.GetMulti(ctx, strIDs)
	if err != nil {
		log.Errorf(ctx, err.Error())
		return handleError(ctx, err)
	}

	var results []*dataloader.Result
	for _, user := range users {
		result := dataloader.Result{
			Data:  user,
			Error: nil,
		}
		results = append(results, &result)
	}

	log.Infof(ctx, "[GetUsersBatchFunc] batch size: %d", len(results))
	return results
}
```

#### リゾルバーを実装する

リゾルバーは以下のように実装します。
contextから`Loader`を取り出します。
受け取ったパラメータを[Key](https://github.com/graph-gophers/dataloader/blob/master/key.go#L4-L9)につめて`Loader`に渡します。
この時点では、Datastoreとの通信は発生せずサンクとしてためられていきます。先に説明したバッチ処理用の関数でまとめて処理されます。

https://github.com/monmaru/gae-graphql/blob/master/application/gql/resolver.go#L160-L179

```golang
func (r *graphQLResolver) getUsersBatch(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
	defer log.Duration(ctx, time.Now(), "[graphQLResolver.getUsersBatch]")
	strID, ok := params.Args["id"].(string)
	if !ok {
		return nil, errors.New("invalid id")
	}

	key := newGetUserKey(strID)
	v := ctx.Value(GetUsersKey)
	loader, ok := v.(*dataloader.Loader)
	if !ok {
		return nil, errors.New("loader is empty")
	}

	thunk := loader.Load(ctx, key)
	return func() (interface{}, error) {
		return thunk()
	}, nil
}
```

#### dataloader.Loaderを生成

いろいろと順番前後していますが`dataloader.Loader`に最初に説明したバッチ処理関数をわたして生成します。
`Loader`にはキャッシュ機能があるので、HTTPリクエストに対して一つの`Loader`が生成されるようにしています。
そのため、contextに対して`Loader`をセットしています。
私はミドルウェアを実装して対応しました。

https://github.com/monmaru/gae-graphql/blob/master/interfaces/middleware/inject.go#L31-L55

```golang

func (i *contextInjector) Inject(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := i.setupContext(r)
		defer log.Duration(ctx, time.Now(), fmt.Sprintf("[%s]", r.URL.Path))
		r = r.WithContext(ctx)
		next.ServeHTTP(w, r)
	})
}

func (i *contextInjector) setupContext(r *http.Request) context.Context {
	ctx := r.Context()
	ctx = context.WithValue(
		ctx,
		gql.GetUsersKey,
		dataloader.NewBatchedLoader(gql.GetUsersBatchFunc(i.ur)))
// 省略！！
}
```

### 並行処理でがんばる
DatastoreをGoで使う場合、クエリでOR検索できなかったりするので、遅延評価でまとめて処理しても意味がないことがあります。
そういったところではヘルパー関数を作り、goroutineで並行に処理することでパフォーマンスを向上させました。
Datastoreに対して並行にリクエストするのは定石です。

```golang
func concurrentResolve(fn graphql.FieldResolveFn) graphql.FieldResolveFn {
	return func(params graphql.ResolveParams) (interface{}, error) {
		type result struct {
			data interface{}
			err  error
		}
		ch := make(chan *result, 1)
		go func() {
			defer close(ch)
			data, err := fn(params)
			ch <- &result{data: data, err: err}
		}()
		return func() (interface{}, error) {
			r := <-ch
			return r.data, r.err
		}, nil
	}
}
```

### もう一度Traceを見てみる
単純な例なので劇的な変化はありませんが、`GetMulti`によってまとめて処理されていることが分かります。
また、`Query.GetAll`が複数同時に呼び出されています。
処理時間はだいたい半分になりました。
複雑なリクエストであれば効果はかなり大きいはずです。
<img width="816" alt="fast-trace.png" src="https://qiita-image-store.s3.amazonaws.com/0/16865/9f05ef16-4078-a971-0473-dd4ba622ee75.png">

## やりのこしたこと
Datastoreを使って[Relay-Style Cursor Pagination](https://facebook.github.io/relay/graphql/connections.htm)を実装してみたかったのですが時間切れです。

## まとめ
GAEとDatastoreを使ってGraphQL APIを実装してみました。
Datastoreは、並行処理も気軽にできるしRDBよりも相性良いのではと感じました。
Stackdriver Traceを使えばパフォーマンスチューニングも容易にできますし、非常に開発しやすいです。

GraphQLやApp Engineに興味のある方は是非遊んでみてください ❗  

誤りやアドバイス等ありましたらコメントお願いしますm(_ _)m