GraphQL in wonder typed land

When

2016/11/04

At

[\#10 市ヶ谷Geek★Night「型のあるフロントエンドの世界〜フロントエンド・フロンティア〜」](http://ichigayageek.connpass.com/event/43149/)

---

# 今回のGitHubリポジトリ

[ovrmrw/graphql-server-dataloader](https://github.com/ovrmrw/graphql-server-dataloader)

リポジトリにデモサイトへのリンクがあります。

===

話す内容

- GraphQLが型を持っていることで何が嬉しいか。
- DataLoaderの威力。

---

## 自己紹介

ちきさん
(Tomohiro Noguchi)

twitter: [@ovrmrw](https://twitter.com/ovrmrw)

ただのSIer。

Angular Japan User Group (ng-japan)スタッフ。

![3a2512bb-aa72-4515-af42-1f1721252f39.jpg](https://qiita-image-store.s3.amazonaws.com/0/74793/c753b39c-1581-ea1d-87d1-181f37bda60d.jpeg)

---

### Angularとても簡単です。みんな使いましょう。

---

# GraphQL is what?

## "A query language for your API"

- Facebookが作ったライブラリ。
- NetflixのFalcorに似ているけどもっと高機能。
- Falcorはメンテされているか怪しいがこちらは活発。
- 要求した形のJSONを返してくれる。
- クライアント(スマホ等)は**リクエストを1回送るだけ**で完成版のJSONを受け取れる。
- データの集約作業はGraphQLサーバーが担当する。

[GraphQL公式 http://graphql.org/](http://graphql.org/)

![12972006.png](https://qiita-image-store.s3.amazonaws.com/0/74793/e09bfec4-75f9-d349-abc1-453d476d1d13.png)

---

リクエスト → → 
(id 1番のuserちょうだい)

```txt
{
  user (id: "1") {
    id
    name
    age
  }
}
```

→ → レスポンス

```json
{
  "user" {
    "id": "1",
    "name": "Tarou",
    "age": 20
  }
}
```

---

すごいリクエスト → → 
(id 1番のuserの友達の友達の友達の友達の…)

```txt
{
  user(id:"1") {
    id
    name
    age
    address {
      zip
      street
    }
    hobby {
      name
    }
    follow {
      id
      name
      age
      address {
        zip
        street
      }
      hobby {
        name
      }
      follow {
    	id
        name
        hobby {
          name
        }
        follow {
          id
          name
          hobby {
            name
          }
          follow {
            id
            name
            hobby {
              name
            }
          }
        }
      }
    }
  }
}
```

---

→ → すごいレスポンス

**クライアントは1回のリクエストを送るだけ。**

```json
{
  "data": {
    "user": {
      "id": "1",
      "name": "Tarou●",
      "age": 20,
      "address": {
        "zip": "111",
        "street": "Shibuya"
      },
      "hobby": [
        {
          "name": "movie"
        },
        {
          "name": "swimming"
        },
        {
          "name": "hiking"
        }
      ],
      "follow": [
        {
          "id": "2",
          "name": "Hanako■",
          "age": 30,
          "address": {
            "zip": "222",
            "street": "Marunouchi"
          },
          "hobby": [
            {
              "name": "movie"
            },
            {
              "name": "swimming"
            }
          ],
          "follow": [
            {
              "id": "3",
              "name": "John▲",
              "hobby": null,
              "follow": [
                {
                  "id": "1",
                  "name": "Tarou●",
                  "hobby": [
                    {
                      "name": "movie"
                    },
                    {
                      "name": "swimming"
                    },
                    {
                      "name": "hiking"
                    }
                  ],
                  "follow": [
                    {
                      "id": "2",
                      "name": "Hanako■",
                      "hobby": [
                        {
                          "name": "movie"
                        },
                        {
                          "name": "swimming"
                        }
                      ]
                    },
                    {
                      "id": "3",
                      "name": "John▲",
                      "hobby": null
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          "id": "3",
          "name": "John▲",
          "age": 40,
          "address": {
            "zip": "333",
            "street": "Sugamo"
          },
          "hobby": null,
          "follow": [
            {
              "id": "1",
              "name": "Tarou●",
              "hobby": [
                {
                  "name": "movie"
                },
                {
                  "name": "swimming"
                },
                {
                  "name": "hiking"
                }
              ],
              "follow": [
                {
                  "id": "2",
                  "name": "Hanako■",
                  "hobby": [
                    {
                      "name": "movie"
                    },
                    {
                      "name": "swimming"
                    }
                  ],
                  "follow": [
                    {
                      "id": "3",
                      "name": "John▲",
                      "hobby": null
                    }
                  ]
                },
                {
                  "id": "3",
                  "name": "John▲",
                  "hobby": null,
                  "follow": [
                    {
                      "id": "1",
                      "name": "Tarou●",
                      "hobby": [
                        {
                          "name": "movie"
                        },
                        {
                          "name": "swimming"
                        },
                        {
                          "name": "hiking"
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  }
}
```

---

# Apollo is what?

## "Tools & products for GraphQL"

- 本家FacebookもGraphQL用のツールを作っているが、これはサードパーティーのライブラリ。サーバーサイド向けとクライアント向け両方ある。
- 本家はReactと連携するRelayというクライアントライブラリを提供しているが、こちらはAngular, iOS, Android向けがある。
- 僕はAngular派なので現状クライアントはApolloを使うしかない。
- クライアントでApolloを使うのでなんとなくサーバーサイドもApolloを使っている。
- 更新速いし活発(?)なので個人的には好き。

[Apollo公式 http://www.apollodata.com/](http://www.apollodata.com/)

![apollo-logo.png](https://qiita-image-store.s3.amazonaws.com/0/74793/31c9c15b-1403-423c-10d7-041a743e2032.png)

---

- 実はApolloは公式サイトより[MediumのApolloブログ](https://dev-blog.apollodata.com/)の方が情報が充実している。

- でも公式サイトもいつの間にか更新されているのでたまに目を通すと良い。

- あるとき急にガラッと変わっていたりするので楽しい。

- "apollo-server"だったライブラリが"graphql-server"に変わったりした。

- 今朝も"apollo-client"がまあまあ変わってた。

---

### Apolloにはクライアント向けライブラリもあるけど、今日はサーバーサイドライブラリの話をします。

---

[Apolloサーバーサイドライブラリのドキュメント](http://dev.apollodata.com/tools/)

## graphql-tools

- 公式のGraphQL.jsよりもGraphQLを書きやすく読みやすく書くことを目指したもの、だと思う。
- なんだか色々機能ある。
- ドキュメント読んで…

## graphql-server-{express,hapi,koa}

- GraphQLが動くサーバーを作る。

===

GitHubリポジトリのソースコードをざっと眺めてもらうとなんとなくわかるかと思います。 
 
---

GraphQLサーバーはこれだけ。普通にただのサーバーサイドアプリ(hapi.js)。
TypeScriptで書くと型の恩恵を受けられる。

```ts:server.ts
const server = new Hapi.Server();

const HOST = '0.0.0.0';
const PORT = process.env.PORT || 3000;

server.connection({
  host: HOST,
  port: PORT,
});

server.register({
  register: graphqlHapi,
  options: {
    path: '/graphql',
    graphqlOptions: (request: Hapi.Request) => {
      console.log('='.repeat(80));
      const context: Context = Object.assign(request, { loaders: createLoaders() });
      return {
        schema: executableSchema,
        context
      }
    },
    route: {
      cors: true
    }
  },
});

server.register({
  register: graphiqlHapi,
  options: {
    path: '/graphiql',
    graphiqlOptions: {
      endpointURL: '/graphql',
    },
  },
});

server.start((err) => {
  if (err) {
    throw err;
  }
  console.log(`Server running at: ${server.info.uri}`);
});
```

---

### TypeScriptすごくいいです。みんな使いましょう。

---

# GraphQLサーバーを書く

- schemaを書く。
- schemaに対応するTypeScriptのinterfaceを書く。
- resolverを書く。
- dataLoaderを書く。
- connectorを書く。(各DB向けのfetchコード)

---

## schemaを書く
GraphQL流の型付き言語みたいな感じ。

```ts:schema.ts
export const schema = [`

# type definition of User 
type User {
  id: ID!
  name: String!
  age: Int
  address: Address
  # other users who current user follows.
  follow: [User]
  # current user's hobbies.
  hobby: [Hobby]
}

type Address {
  zip: String
  street: String
}

type Hobby {
  id: ID!
  name: String!
}

type RootQuery {
  users: [User]
  user(id: ID!): User
}

schema {
  query: RootQuery
}

`];
```

---

## schemaに対応するTypeScriptのinterfaceを書く

```ts:schema.ts
export interface User {
  id: ID;
  name: string;
  age?: number;
  address?: Address;
  follow?: ID[];
  hobby?: ID[];
}

export interface Address {
  zip?: string;
  street?: string;
}

export interface Hobby {
  id: ID;
  name: string;
}

type ID = string;
```

---

## 【schemaの解説】

まずschemaというものを定義する。
queryの型は`RootQuery`である。実際には他にmutationとか色々ある。

```txt
schema {
  query: RootQuery
}
```

次に`RootQuery`という型の中身を定義する。
usersは`User`型の配列を返すリゾルバー。配列は`[]`で表す。
userはidを引数に取って`User`型を返すリゾルバー。`!`は省略不可の意味。

```txt
type RootQuery {
  users: [User]
  user(id: ID!): User
}
```

今度は`User`という型を定義する。
`Address`,`Hobby`はいずれも独自の型である。

```txt
type User {
  id: ID!
  name: String!
  age: Int
  address: Address
  follow: [User]
  hobby: [Hobby]
}
```

`Address`型を定義する。

```txt
type Address {
  zip: String
  street: String
}
```

`Hobby`型も定義する。

```txt
type Hobby {
  id: ID!
  name: String!
}
```

---

このようにクエリに必要となる型を全て定義していく。ただし文字列として。

TypeScriptの型は別途書かなければならない。

この辺なんとかなったらみんな幸せになれそう。

===

schemaがあると何が嬉しいって、GraphiQLで入力補完が利く。

[GraphiQLサンプル](https://graphql-server-dataloader.azurewebsites.net/graphiql)

---

## resolverを書く

schemaで定義した通りの結果が返るような関数を書く。

resolverの基本形
`fieldName: (root, args, context, info) => result`

`context`はrequestオブジェクトだと思っておけばOK。

```ts:resolvers.ts
export const resolverMap = {

  RootQuery: {
    users(root: any, args: {}): Promise<User[]> {
      return getUserIds().then(ids => userLoader.loadMany(ids));
    },

    user(root: any, args: { id: string }): Promise<User[]> {
      return userLoader.load(args.id);
    },
  },

  User: {
    follow(user: User, args: {}): Promise<User[]> | null {
      return user.follow ? userLoader.loadMany(user.follow) : null;
    },

    hobby(user: User, args: {}): Promise<Hobby[]> | null {
      return user.hobby ? hobbyLoader.loadMany(user.hobby) : null;
    }
  }

};
```

---

【schemaとresolverの対応の解説】(1/2)

こういうschemaがあったら…

```
type RootQuery {
  users: [User]
  user(id: ID!): User
}
```

こういうresolverを書く。

```ts
  RootQuery: {
    users(root: any, args: {}): Promise<User[]> {
      return getUserIds().then(ids => userLoader.loadMany(ids));
    },

    user(root: any, args: { id: string }): Promise<User[]> {
      return userLoader.load(args.id);
    },
  },
```

`RootQuery`と`users`,`user`の階層構造が一致している！

関数の第一引数`root`は親オブジェクトを指す。この場合はrootなので`undefined`となる。

---

【schemaとresolverの対応の解説】(2/2)

こういうschemaがあったら…

```
type User {
  (省略)
  follow: [User]
  hobby: [Hobby]
}
```

こういうresolverを書く。

```ts
  User: {
    follow(user: User, args: {}): Promise<User[]> | null {
      return user.follow ? userLoader.loadMany(user.follow) : null;
    },

    hobby(user: User, args: {}): Promise<Hobby[]> | null {
      return user.hobby ? hobbyLoader.loadMany(user.hobby) : null;
    }
  }
```

`User`と`follow`,`hobby`の階層構造が一致している！

関数の第一引数`user`は親オブジェクトを指す。この場合は`User`型のオブジェクトとなる。

---

### 実際にクエリをリクエストときのGraphQLサーバーのログを見てみる。

(DataLoaderによるBatching, Caching無し)

---

リクエスト → →

```
{
  user(id:"1") {
    ...userFragment
    follow {
      ...userFragment
      follow {
        ...userFragment
        follow {
          ...userFragment
          follow {
            ...userFragment
          }
        }
      }
    }
  }
}

fragment userFragment on User {
  id
  name
  address {
    zip
    street
  }
  hobby {
    name
  }  
}
```

これはfragmentという機能を使ったクエリの書き方。

---

GraphQLサーバーのログ

```
userLoader fetch: { id: '1', name: 'Tarou●' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '3', name: 'hiking' }
userLoader fetch: { id: '2', name: 'Hanako■' }
userLoader fetch: { id: '3', name: 'John▲' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
userLoader fetch: { id: '3', name: 'John▲' }
userLoader fetch: { id: '1', name: 'Tarou●' }
userLoader fetch: { id: '1', name: 'Tarou●' }
userLoader fetch: { id: '2', name: 'Hanako■' }
userLoader fetch: { id: '3', name: 'John▲' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '3', name: 'hiking' }
hobbyLoader fetch: { id: '3', name: 'hiking' }
userLoader fetch: { id: '2', name: 'Hanako■' }
userLoader fetch: { id: '3', name: 'John▲' }
userLoader fetch: { id: '3', name: 'John▲' }
userLoader fetch: { id: '1', name: 'Tarou●' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '1', name: 'movie' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '2', name: 'swimming' }
hobbyLoader fetch: { id: '3', name: 'hiking' }
```

---

### これは…！！！ 

---

# where DataLoader comes into play

---

## dataLoaderを書く

new DataLoader(callback) に登録するコールバックの基本形
`(keys: T[]) => Promise<R[]>`

*"(例えば) stringの配列を受け取って、Userの配列をPromiseで返す関数"*

詳細はこれ読んで → [facebook/dataloader](https://github.com/facebook/dataloader)

```ts:loaders.ts
export function createLoaders(cache: boolean = true, batch: boolean = true): Loaders {
  const options = { cache, batch };
  return {
    userLoader: new DataLoader(userLoaderCallback, options),
    hobbyLoader: new DataLoader(hobbyLoaderCallback, options),
  }
}

function userLoaderCallback(keys: string[]): Promise<User[]> {
  return getUsersConnector(keys);
}

function hobbyLoaderCallback(keys: string[]): Promise<Hobby[]> {
  return getHobbiesConnector(keys);
}
```

```ts:firebase-connectors.ts
export async function getUserIdsConnector(): Promise<string[]> {
  const snapshot = await firebase.database().ref('users').once('value') as Snapshot;
  const users: User[] = lodash.toArray<User>(snapshot.val()).filter(obj => !!obj);
  users.forEach(user => assert(lodash.isObject(user) && 'id' in user));
  const ids: string[] = users.map(user => user.id);
  return ids;
}

export async function getUsersConnector(keys: string[]): Promise<User[]> {
  const snapshotPromises = keys.map(key => firebase.database().ref('users/' + key).once('value') as Promise<Snapshot>);
  const snapshots = await Promise.all(snapshotPromises);
  const users: User[] = snapshots.map(snapshot => snapshot.val());
  users.forEach(user => assert(lodash.isObject(user) && 'id' in user));
  console.log('userLoader fetch:', ...users.map(user => ({ id: user.id, name: user.name })));
  return users;
}

export async function getHobbiesConnector(keys: string[]): Promise<Hobby[]> {
  const snapshotPromises = keys.map(key => firebase.database().ref('hobby/' + key).once('value') as Promise<Snapshot>);
  const snapshots = await Promise.all(snapshotPromises);
  const hobbies: Hobby[] = snapshots.map(snapshot => snapshot.val());
  hobbies.forEach(hobby => assert(lodash.isObject(hobby) && 'id' in hobby));
  console.log('hobbyLoader fetch:', ...hobbies.map(hobby => ({ id: hobby.id, name: hobby.name })));
  return hobbies;
}
```

---

DataLoaderを使うとさっきのアレがこう！

```
userLoader fetch: { id: '1', name: 'Tarou●' }
hobbyLoader fetch: { id: '1', name: 'movie' } { id: '2', name: 'swimming' } { id: '3', name: 'hiking' }
userLoader fetch: { id: '2', name: 'Hanako■' } { id: '3', name: 'John▲' }
```

30回のfetchが6回に！
通信コストの大幅削減！

ちなみにこれは「クライアント←→GraphQLサーバー」のログではなく、「GraphQLサーバー←→DBサーバー」のログです。

---

## DataLoaderを併用することでGraphQLは威力を発揮する

---

### GraphQLかなりいいです。みんな使いましょう。

---

今回のソースコードは全て下記のリポジトリからの引用です。

[ovrmrw/graphql-server-dataloader](https://github.com/ovrmrw/graphql-server-dataloader)

---

#### Thanks!