RelayにのっとったGraphQLを、GAE/Goで扱ってみた

この記事は [Go Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go) 20日目の記事です。

# はじめに

gRPCやProtocol Buffersを扱っている、またRESTから置き換えているという話を様々な記事やイベントの発表でよく聞きます。
しかし、日本でGraphQLを導入したという話は、Webフロント（特にReactを使っているところ）のイベントなどのスライドを見ると出てくる程度で、プロダクトで使ってるとか、なかなか言及される記事もポツポツとしか存在しないので、圧倒的に情報量が少ない、そもそもGraphQLを使ってみる障壁が高い（海外の記事は多く上がっているので、そっちを見ればできなくはない）ので、僕もまだまだ始めたばかりですが、この記事でGAE/GoとGraphQLを触ってみたい人が増えると嬉しいです。



# GraphQLは何がいいの？

[GraphQL](https://graphql.org/)についてはここでは詳しい説明はしませんが、APIからデータを取得するためのクエリ言語です。

利点は以下のようなものがよく挙げられます。

- クライアントが欲しいデータのみを取得できる
- 一回のクエリで様々な種類のデータを取得することが可能
- サーバーの都合とクライアントの都合が分離できる

GraphQLの記事などを少し漁ると同じような利点が書かれていると思いますが、使ってみるとよくわかります。
個人的には最もメリットだと感じているのは、三つ目の**サーバーの都合とクライアントの都合が分離できる**
です。
なぜなら、これまでサーバーを開発する上で、フロント側の都合を考慮して、ビジネスロジックとしては関係ないが、実装面からの観点で、データ構造を非正規化が必要だったり、中間値をとるためのAPIを作成しなくてはならなくなり、それに伴って、サーバーのコードだけ見て、なぜこうしたのかが分からないものが多々存在しました。
しかし、GraphQLを使用すると、サーバーのアプリケーション開発者はそれぞれのデータを返すことを考えるだけで、基本的には大丈夫です。なぜなら、フロントアプリケーション開発者がqueryでデータフォーマットを決めて、それに従った形に、詰め直す処理はGraphQL側がやってくれるからです。

しかし、これだけ聞くとなかなか導入や試すのが難しいように感じるかもしれませんが、
GraphQLは何か特殊なデータフォーマットでレスポンスが返ってくるわけでは無く、**JSON** で返ってきます。
つまり、現行のサービスで、JSONで通信をしているところはもしも、GraphQLに置き換えたいとなった場合、はじめは最小の変更から始められることができる可能性が高いということです。

（Google trendsでgRPCとGraphQLを検索してみると以下のような結果になり、やはり、日本ではあまり人気はないのかもしれないです、、）
<img width="487" alt="スクリーンショット 2019-12-20 10.30.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/83818c5a-fe81-c5c7-00c5-e6895115f359.png">
<img width="484" alt="スクリーンショット 2019-12-20 10.30.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/d109511f-dfa2-6ab4-dc7c-309ddde1632e.png">


そのほか、なぜGraphQLができたのかとかは[公式のドキュメント](https://graphql.org/)やFacebookが出している[こちらの記事](https://engineering.fb.com/core-data/graphql-a-data-query-language/)をご覧ください！

今年のアドベントカレンダーもあるので、GraphQLそのものをもっと詳しく知りたい方はこちらをご覧ください！
[GraphQL Advent Calendar 2019](https://qiita.com/advent-calendar/2019/graphql)



# Go x GraphQL

Goで扱うためのライブラリもちろんはいくつか存在しています。

- [graphql-go/graphql](https://github.com/graphql-go/graphql)
- [samsarahq/thunder](https://github.com/samsarahq/thunder)
- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)
- [99designs/gqlgen](https://github.com/99designs/gqlgen)

現在開発がここ最近でも行われているものはこの４つです。今回使うのは４つ目のgqlgenです！

おそらく、他の多くの記事でもこちらを使っているとは思いますが、僕の中での理由はこんな感じです

- GraphQLスキーマからクエリ解釈して、それに応じたメソッド（リゾルバー）を呼ぶところまでを自動生成してくれる
- 他のライブラリと違って、スキーマファーストなライブラリとなっている
- 開発が最も盛んで、ドキュメントも揃っている

コードを自動生成してくれるので、GraphQLをあまり理解していなくても、Goを知っている人ならば、用意されたinterfaceの実装を用意してあげれば動くので、シュッと試すことを可能にしてくれます。

gqlgenが、この四つのライブラリの[機能比較表](https://gqlgen.com/feature-comparison/)を書いている(gqlgenが書いているので、少し偏っている可能性はありますが）参考にして、ベストなライブラリを選択してください。


# Relayのスキーマ仕様

GraphQLは非常に表現力が高いので、やろうと思えば様々なことができてしまいます。しかしするとqueryやschemaが複雑化してしまい、秩序のないものになってしまいます。そのため、ある一定のルールにしたがってそれ元にして、各サービスに適用させていくのが良いと思います。そして、
[Relay](https://relay.dev/)はFacebookが開発しているReactでGraphQLを扱うためのクライアントライブラリです。その中に、RelayでGraphQLをよりうまく活用するための[スキーマの仕様が定義](https://relay.dev/docs/en/graphql-server-specificatio)されています。これにまずは従って書いていき、各サービスで必要なものを適用し足りないものを追加する方法が良いです。



- interface Node を定義しエンティティはidにって引けるようにする
- リストを返す場合は[Cursor Connections](https://relay.dev/docs/en/graphql-server-specification#connections)に従う
- Mutationの名前は動詞、引数の型名にInputサフィックス、返り値の型名にPayloadサフィックスをつける
- Mutationの入力と出力を紐付けて調整するために使用されるクライアント変換識別子が必要になる場合がある
(これは[v7.1.0](https://relay.dev/docs/en/v7.1.0/graphql-server-specification)まではMutationの引数と返り値に識別子clientMutationIdが必要と書いてあるが、[v8.0.0](https://relay.dev/docs/en/graphql-server-specification#mutations)ではその記載は消えている）



# Relay x GAE/Go

以下での話はGAE/Goを扱う話なので、datastoreを使った話になります。

## interface Node を定義しエンティティはidにって引けるようにする
まず一つ目の**interface Node を定義しエンティティはidにって引けるようにする**で
GraphQLではidは全てのエンティティの中で固有でなければなりません。
つまりどういうことかというと、UserとItemがあってそれぞれ、idを持っている時、

```
User.ID = 1
Item.ID = 1
```

これは許されなません。それぞれ固有でなければならないので、

```
User.ID = "User:1"
Item.ID = "Item:1"
```

などのように必ずそのエンティティが一意に確定しなくてはなりません

そして、Relayではinterface Nodeを定義して、idによって各エンティティを引っ張ってこれるようにしなくてはいけないのです。

つまり、一意に確定するのはUUIDなどを使えば、IDとエンティティは一対一になりますが、それではIDだけを使って引いてくることはできません。なぜなら、そのIDがどの型のIDなのかを判別する術がないからです。

なので、GAEを使う場合、datastoreを使うことを想定した場合はIDはKind名と各Kindの識別子（idかname)を持った文字列にするといいと思います。
そうすれば、どのKindで引くのかまで明確にわかるので、idだけでもentityを引っ張ることが可能になります。



## リストを返す場合はCursor Connectionsに従う


Relayでは、あるオブジェクトをリストで返したい場合に、ある一定のルールを設けて返すことによって、ページングや一覧を扱う処置をやりやすくしてくれています。その時に必要となる型は次の通りです。

- XXXEdge型: CursorとXXX型を持つ型
- PageInfo: ページ情報を持つ型
- XXXConnection型: PageInfoとXXXEdgeのリストとその他必要なフィールドを持つ型

これらの型を使ってリストを以下の手順でqueryのschemaを定義する
- リストを返すところをXXXConnectionを返すようにする
- queryのフィールドに引数に{first: Int, after: String, last: Int, before: String}

first/after、last/beforeのセットで扱います。
- first/last: after/before(cursor)から何個のオブジェクトを取ってくるか
- after: cursorを受け、リストを取得しはじめるオブジェクトの開始位置を示す
- before: cursorを受ける、リストを取得しはじめるオブジェクトの開始位置を示す。そして、afterをは逆方向


```graphql
interface Node {
    id: ID!
}

type User implements Node {
    id: ID!
}

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

type UserEdge {
    cursor: String!
    node: User
}

type UserConnection {
    edges: [UserEdge]
    pageInfo: PageInfo!
}

type Query {
  users(first: Int, after: String, last: Int, before: String)
}
```

ここで登場するCursorをdatastoreのCursorを使用することができます。
しかし、datastoreのCursorを扱う場合気を付けなければならない点があります。

- datastoreのCursorは同じクエリでないと使うことができない
- datastoreのCursorは暗号化をデコードして結果のエンティティに関する情報を取得できる
- データの更新

一つ一つ説明していきます

### datastoreのCursorは同じクエリでないと使うことができない

[datastoreカーソル制限](https://cloud.google.com/datastore/docs/concepts/queries?hl=ja#cursors_limits_and_offsets) より以下の状況のみ同じCursorを扱うことができます

- 実行したプロジェクトと同じプロジェクトのCursor
- 開始カーソル、終了カーソル、オフセット、条件の更新
- 元クエリが"__key__"で最後に並び替えしている場合は、逆引きクエリで使用できる

これ以外のものでは基本的に同じカーソルを扱うことはできません
（しかし、逆引きクエリの場合エラーにならない場合が僕が確認した限りではあるのですが、公式ではカーソルは使用できなくなると書かれているので使わない方が良い）


### datastoreのCursorは暗号化をデコードして結果のエンティティに関する情報を取得できる

これはCursorの中に以下の情報が含まれている

- ProjectID
- エンティティの種類
- キー名または数値 ID
- 祖先キー
- プロパティ

など、つまり、そのタイミングのエンティティのデータが入ってます。なので、これらの情報がばれたくない場合は、何かしら暗号化したものなどをクライアントに返す必要があります。


### データの更新

カーソルは最後の結果が返された後の結果のリスト内の位置を表してます。
これより以下のルールがあります

- データが更新された場合カーソル位置より後の結果で発生した変化は検知する
- カーソルより後の結果を取ってきても新しい結果は返さない

これらの特徴があるので、カーソルで取ってきてしまったものは変わらないですが、これからとるものに関しては変化してくれるので、それに関しては気にしなくても大丈夫です。



以上の制限を踏まえてGAE/GOで実装していく必要があります！


# シンプル実装

試しにシンプルなGraphQL＋GAE/GOの[実装したもの](https://github.com/Yamashou/gae-relay/tree/master)を見ていきます

僕は基本的にdatastoreとgoを使う時は[go.mercari.io/datastore](https://github.com/mercari/datastore)を使わせていただいています。
その都合で、UserとEventのstructのidにboomタグがついています。

※今回、以上で述べたこと以外のことや複雑なクエリが出てくるようなことはしていません(dataloadenの導入やCashや、親の親からデータを取ってきたり）
## ディレクトリ構成

```

.
├── app.yaml
├── datastore
│   └── datastore.go
├── go.mod
├── go.sum
├── gqlgen
│   ├── generated.go
│   └── models_gen.go
├── gqlgen.yml
├── main.go
├── model
│   ├── event.go
│   ├── page.go
│   └── user.go
├── schema.graphql
└── server
    └── resolver_gen.go

```


## スキーマ

今回datastoreの挙動上last/beforeを同じカーセルで実現するのが難しかったので、省略したものにしました。
そのため、HasPreviousPageも省略しています。


```graphql

interface Node {
    id: ID!
}

type PageInfo {
    hasNextPage: Boolean!
    startCursor: String
    endCursor: String
}

type User implements Node {
    id: ID!
    name: String!

    events(first: Int!, after: String): EventConnection!
}


type Event implements Node {
    id: ID!
    userID: ID!
    description: String!
}

type UserEdge {
    cursor: String!
    node: User
}

type UserConnection {
    edges: [UserEdge]
    pageInfo: PageInfo!
}

type EventEdge {
    cursor: String!
    node: Event
}

type EventConnection {
    edges: [EventEdge]
    pageInfo: PageInfo!
}


input CreateUserInput {
    name: String!
}

type CreateUserPayload {
    user: User!
}

input CreateEventInput {
    userID: ID!
    description: String!
}

type CreateEventPayload {
    event: Event!
}

type Mutation {
    createUser(input: CreateUserInput!): CreateUserPayload
    createEvent(input: CreateEventInput!): CreateEventPayload
}

type Query {
    node(id: ID!): Node
    user(id: ID!): User
    users(first: Int!, after: String): UserConnection!
}
```



## gqlgen設定

生成されるコードはgqlgenディレクトリ以下に入れて、このアプリケーション内で使い回す構造体をmodelsに入っているので、autobindを指定しておくと、その構造体はmodels_gen.goで生成されず、指定した方の構造体を使ってくれるようになります！

```yaml
schema:
- schema.graphql
exec:
  filename: gqlgen/generated.go
model:
  filename: gqlgen/models_gen.go
resolver:
  filename: server/resolver_gen.go
  type: Resolver
autobind:
  - github.com/Yamashou/gae-relay/model

```


## Model

モデルと言っても、ビジネスロジックも特にないので、structの定義で、基本的には自動生成させずに自分で描きたいものを定義します。

以下のUserを見てみるとスキーマの方に定義していた、eventsがありません。
これはeventsはeventsを取得するresolverを定義したいので、書いていません。
こうしておくと、gqlgenがインターフェースにEventsリゾルバーを追加してくれます

```go

// user.go
package model

import (
	"fmt"
	"strings"

	"github.com/google/uuid"
)

type UserID string

func newUserID() UserID {
	return UserID(fmt.Sprintf("User:%s", uuid.New().String()))
}

func IsUserID(s string) bool {
	return strings.Contains(s, "User")
}

func NewUser(name string) *User {
	return &User{
		ID:   newUserID(),
		Name: name,
	}
}

type User struct {
	ID   UserID `boom:"id" json:"id"`
	Name string `json:"name"`
}

func (u *User) IsNode() {}

type UserConnection struct {
	Edges    []*UserEdge `json:"edges"`
	PageInfo *PageInfo   `json:"pageInfo"`
}

type UserEdge struct {
	Cursor string `json:"cursor"`
	Node   *User  `json:"node"`
}
```


```go

// event.go

package model

import (
	"fmt"
	"strings"

	"github.com/google/uuid"
)

type EventID string

func newEventID() EventID {
	return EventID(fmt.Sprintf("Event:%s", uuid.New().String()))
}

func IsEventID(s string) bool {
	return strings.Contains(s, "Event")
}

func NewEvent(userID UserID, description string) *Event {
	return &Event{
		ID:          newEventID(),
		UserID:      userID,
		Description: description,
	}
}

type Event struct {
	ID          EventID `boom:"id" json:"id"`
	UserID      UserID  `json:"userID"`
	Description string  `json:"description"`
}

func (e *Event) IsNode() {}

type EventConnection struct {
	Edges    []*EventEdge `json:"edges"`
	PageInfo *PageInfo    `json:"pageInfo"`
}

type EventEdge struct {
	Cursor string `json:"cursor"`
	Node   *Event `json:"node"`
}

```

## Datastore

これは結構大きくなってしまっているので複数取得する部分のGetUsersを見てみます。

```go
package datastore

import (
	"context"

	"google.golang.org/api/iterator"

	"github.com/Yamashou/gae-relay/model"

	"golang.org/x/xerrors"

	"go.mercari.io/datastore/boom"

	"go.mercari.io/datastore"
)

type Client struct {
	client datastore.Client
}

...

func (c *Client) GetUsers(ctx context.Context, limit int, cursor string) (*model.UserConnection, error) {
	// 次のページが存在するか確認するため1件多く取得する
	limitPlusOne := limit + 1
	bm := boom.FromClient(ctx, c.client)
	q := bm.NewQuery(bm.Kind(model.User{})).
		Limit(limitPlusOne)
	if cursor != "" {
                // カーソルが空出ない時はそれをセットする
		cur, err := c.client.DecodeCursor(cursor)
		if err != nil {
			return nil, xerrors.Errorf(": %w", err)
		}
		q = q.Start(cur)
	}

	var edges []*model.UserEdge
	it := bm.Run(q)
	for {
		var user model.User
		_, err := it.Next(&user)
		if xerrors.Is(err, iterator.Done) {
			break
		}
		if err != nil {
			return nil, xerrors.Errorf(": %w", err)
		}

		cursor, err := it.Cursor()
		if err != nil {
			return nil, xerrors.Errorf(": %w", err)
		}

		edge := &model.UserEdge{
			Cursor: cursor.String(),
			Node:   &user,
		}

		edges = append(edges, edge)
	}

	// このページの最初と最後のCursorを返す
	var startCursor, endCursor string
	if len(edges) > 0 {
		startCursor = edges[0].Cursor
		endCursor = edges[len(edges)-1].Cursor
	}

	// 次のページが存在する場合
	var hasNextPage bool
	if len(edges) == limitPlusOne {
		hasNextPage = true
		// 最後の1件は次のページの存在確認用なので除外する
		edges = edges[:len(edges)-1]
	}

	conn := &model.UserConnection{
		Edges: edges,
		PageInfo: &model.PageInfo{
			StartCursor: &startCursor,
			EndCursor:   &endCursor,
			HasNextPage: hasNextPage,
		},
	}

	return conn, nil
}
```


## Resolver

server/resolver_gen.goにこのようなコードが生成されます。
基本的に、この中を実装していけば、GraphQLのapiを作ることができます

```golang

package server

import (
	"context"

	"github.com/Yamashou/gae-relay/gqlgen"
	"github.com/Yamashou/gae-relay/model"
)

// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.

type Resolver struct{}

func (r *Resolver) Event() gqlgen.EventResolver {
	return &eventResolver{r}
}
func (r *Resolver) Mutation() gqlgen.MutationResolver {
	return &mutationResolver{r}
}
func (r *Resolver) Query() gqlgen.QueryResolver {
	return &queryResolver{r}
}
func (r *Resolver) User() gqlgen.UserResolver {
	return &userResolver{r}
}

type eventResolver struct{ *Resolver }

func (r *eventResolver) ID(ctx context.Context, obj *model.Event) (string, error) {
	panic("not implemented")
}
func (r *eventResolver) UserID(ctx context.Context, obj *model.Event) (string, error) {
	panic("not implemented")
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateUser(ctx context.Context, input gqlgen.CreateUserInput) (*gqlgen.CreateUserPayload, error) {
	panic("not implemented")
}
func (r *mutationResolver) CreateEvent(ctx context.Context, input gqlgen.CreateEventInput) (*gqlgen.CreateEventPayload, error) {
	panic("not implemented")
}

type queryResolver struct{ *Resolver }

func (r *queryResolver) Node(ctx context.Context, id string) (gqlgen.Node, error) {
	panic("not implemented")
}
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
	panic("not implemented")
}
func (r *queryResolver) Users(ctx context.Context, first int, after *string) (*model.UserConnection, error) {
	panic("not implemented")
}

type userResolver struct{ *Resolver }

func (r *userResolver) ID(ctx context.Context, obj *model.User) (string, error) {
	panic("not implemented")
}
func (r *userResolver) Events(ctx context.Context, obj *model.User, first int, after *string) (*model.EventConnection, error) {
	panic("not implemented")
}

```

先ほど作った、GetUsersを使用して、Usersリゾルバーを実装は以下のようにします


```go

type Resolver struct {
	DatastoreClient *datastore.Client
}

...

func (r *queryResolver) Users(ctx context.Context, first int, after *string) (*model.UserConnection, error) {
	var cursor string
	if after != nil {
		cursor = *after
	}
	userConnection, err := r.DatastoreClient.GetUsers(ctx, first, cursor)
	if err != nil {
		return nil, xerrors.Errorf(": %w", err)
	}

	return userConnection, nil
}
```



## 動作

以下に動作を確認したgifを載せてありますが、自分で確認したい方は、実装をクローンして、自身のGCPのプロジェクトにデプロイしていただいて、試してください！

[リポジトリ](https://github.com/Yamashou/gae-relay/tree/master)

CreateUser

![create_user.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/225bdf6b-4b49-4d9f-7108-20395501a638.gif)


CreateEvent
![create_event.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/0eb0a051-3142-9f9a-d14f-1d19d8c6c5b8.gif)

id指定でNodeを取得
![create_node.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/8cd13d7f-4a5a-583f-f868-431790c58f9a.gif)

UsersクエリでUserを複数取得する
![get_users.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115970/05448989-2868-01de-7541-d1c1279e1167.gif)

# まとめ

まだまだ、学び始めたばかりですが、単純に日本でなかなかこの辺んお話出てこないので、もしかしたら自分が記事を書いたら、誰かやってくれる人が出てくるんじゃないかという淡い期待のを持ちつつ書きました。
個人的にはとてもGraphQLはいいんじゃないかなと思っているのでこれからも、GoとGraphQLでサーバーアプリケーションを開発していきたいと思います。



# 参考
- https://github.com/vvakame/graphql-with-go-book
- https://www.oreilly.co.jp/books/9784873118932/
- https://relay.dev/
- https://facebook.github.io/relay/graphql/connections.htm
- https://graphql.org/
- https://cloud.google.com/datastore/docs/concepts/queries?hl=en
- https://qiita.com/wawoon/items/d00bd180dcac48a3068e