Apollo+PrismaでGraphQLに入門する

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

先日、とある管理画面を実装後に GraphQL で実装すれば良かったなーと思う事があり、改めて GraphQL に興味が沸き前々から触ってみようと思っていたので、実際に触ってみました。

本当は簡単なTodoアプリを作る予定だったのですが、GraphQLサーバーの触りだけをまとめる感じになってしまいました ( ˆ꒳​ˆ; )

[t-yng/graphql-todo-app](https://github.com/t-yng/graphql-todo-app)

## ライブラリの紹介

* [apollographql/apollo-server](https://github.com/apollographql/apollo-server): GraphQLサーバー
    * 実際にGraphQLのリクエストを受け付けてデータを返すサーバー
    * この記事ではラップされた [apollo-server-express](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-express) を利用していきます。
* [prisma/prisma](https://github.com/prisma/prisma): ORM
    * GraphQLサーバーとデータベースを繋ぐORM
    * prisma自体はライブラリではなくCLIツール

## プロジェクトのセットアップ

```
$ yarn init -y
$ yarn add typescript
$ yarn add -D ts-node
$ yarn tsc --init
```

## apollo server をインストール

```
$ yarn add apollo-server-express express @types/express
```

## Hello World

### 実装

```
$ mkdir src
$ touch src/index.ts
```

```typescript:src/index.ts

import express from 'express';
import { ApolloServer, gql } from 'apollo-server-express';

const typeDefs = gql`
    type Query {
        hello: String
    }
`;

const resolvers = {
    Query: {
        hello: () => 'Hello GrapQL!',
    },
};

const server = new ApolloServer( { typeDefs, resolvers });

const app = express();
server.applyMiddleware({ app });

app.listen({ port: 4000 }, () => {
    console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
});
```

### 実行する

package.json に実行スクリプトを追加して実行します。

```json:package.json
  "scripts": {
    "dev": "ts-node src/index.ts"
  }
```

```bash
$ yarn dev
```

http://localhost:4000/graphql にアクセスして、次のクエリを実行すれば結果が返ってきます。

```graphql
query {
  hello
}
```

## GraphQLのインターフェースを実装してみる

次に簡単なTodoを取得するGraphQLのQueryを自分で実装してみます。

### スキーマを定義
Todoオブジェクトの型定義とそれを取得する Query の型定義を書いていきます。
※ 型定義という言い方が正しいのか不明

```typescript:src/index.ts
const typeDefs = gql`
    type Todo {
        id: Int!
        title: String
    }

    type Query {
        todo(id: Int): Todo
        todoes(title: String): [Todo]
    }
`;
```

### リゾルバを実装

リゾルバとは実際にリクエストを受け取りスキーマ定義に合わせてデータを返す部分です。
仮で変数を定義して値を返してみます。

```typescript:src/index.ts
const todoes = [
    {
        id: 1,
        title: '簡単なタスク',
    },
    {
        id: 2,
        title: '気持ち簡単なタスク',
    },
    {
        id: 3,
        title: 'ヤバいタスク',
    },
    {
        id: 4,
        title: '気づかないフリをしているタスク',
    }
]

const resolvers = {
    Query: {
        todo: (_: any, { id }: { id: number}) => {
            return todoes.find(todo => todo.id === id);
        },
        todoes: (_: any, { title }: { title: string }) => {
            return todoes.filter(todo => todo.title.includes(title))
        },
    },
};
```

### 実行する
それではサーバーを起動して、ここまでの変更を確認してみます。

```zsh
$ yarn dev
ts-node src/index.ts
🚀 Server ready at http://localhost:4000/graphql
```

おー！無事に結果が取得できています。(*ﾟ▽ﾟﾉﾉﾞ☆ﾊﾟﾁﾊﾟﾁ
何か新鮮な体験で凄い楽しいですね！

<img width="1440" alt="スクリーンショット 2019-12-21 1.56.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54838/6696af9d-4d8b-bf68-814c-49aca0152419.png">

## DBと連携する
変数のデータを取得するだけだと味気ないので、折角なら実践的にDBにあるデータを取得するようにしましょう。

### prismaを導入
[prisma/prisma](https://github.com/prisma/prisma) とは GraphQLサーバー と データーベース を繋ぐ ORM です。
モデルの定義を書けば、テーブルのマイグレーションやデータベースへアクセスするためのコードを自動生成をしてくれます。

※ この記事だとTypeORM とかを利用する場合と大差がなく、メリットを全く享受出来ていない気がしてます...

prisma自体はライブラリではなく、CLIツールなので開発用パッケージとしてインストールします。

```
$ yarn add -D prisma
```

### データベースとprismaのセットアップ
ローカルで開発するだけであれば、initコマンドを実行するだけでデータベースのDocker環境をスキャフォールドしてくれます。

今回の設定は `Create new database`、`MySQL`、`Prisma TypeScript Client`を選択しました。

```zsh
$ yarn prisma init .
yarn run v1.19.1
$ /Users/tomohiro/workspace/graphql/graphql-todo-app/node_modules/.bin/prisma init .
? Set up a new Prisma server or deploy to an existing server? Create new database
? What kind of database do you want to deploy to? MySQL
? Select the programming language for the generated Prisma client Prisma TypeScript Client

Created 3 new files:

  prisma.yml           Prisma service definition
  datamodel.prisma    GraphQL SDL-based datamodel (foundation for database)
  docker-compose.yml   Docker configuration file

Next steps:

  1. Open folder: cd .
  2. Start your Prisma server: docker-compose up -d
  3. Deploy your Prisma service: prisma deploy
  4. Read more about Prisma server:
     http://bit.ly/prisma-server-overview

Generating schema... 17ms
Saving Prisma Client (TypeScript) at /Users/tomohiro/workspace/graphql/graphql-todo-app/generated/prisma-client/
```

### Dockerの起動
Prismaサーバーを起動するために、自動生成された docker-compose.yml から Dockerコンテナを起動します。

デフォルトではデータベースコンテナのポートはホスト側に紐づけられていないので、DBにアクセスするために次の箇所をコメントアウトしておきます。

```yml:docker-compose.yml
# Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
ports:
- "3306:3306"
```

```zsh
$ docker-compose up -d
```

### データモデルの定義
データモデルを定義していきます。
prismaはこのモデル定義をDBにテーブルとしてマイグレーションしてくれます。

```:datamodel.prisma
type Todo {
  id: Int! @id(strategy: AUTO)
  title: String!
}
```

### マイグレーションの実行
マイグレーションを実行する前に prisma.yml を変更して、クライアントコードの生成場所とhooksの設定をしておきます。

```yml:prisma.yml
generate:
  - generator: typescript-client
    output: ./src/generated/prisma-client/ # 生成先を src/ に変更

# マイグレーション実行のタイミングでクライアントコードの生成も実行する
hooks:
  post-deploy:
    - prisma generate
```

ではマイグレーションを実行します。

```zsh
$ yarn prisma deploy
yarn run v1.19.1
$ /Users/tomohiro/workspace/graphql/graphql-todo-app/node_modules/.bin/prisma deploy
Creating stage default for service default ✔
Deploying service `default` to stage `default` to server `local` 1.3s

Changes:

  Todo (Type)
  + Created type `Todo`
  + Created field `id` of type `Int!`
  + Created field `title` of type `String!`

...
```

実際にMySQLに接続すれば、default@default/Todo とテーブルが生成されているのが確認できます。

```
host: 127.0.0.1
user: root
password: prisma
```

テスト用にデータを追加しておきます。

```sql
INSERT INTO Todo(title) values 
	('簡単なタスク'),
	('気持ち簡単なタスク'),
	('ヤバいタスク'),
	('気づかないフリをしているタスク');
```

### DBのデータを返す実装に変更
クライアントライブラリが src/generated/prisma-client に自動生成されているので、DBアクセスはそれを利用します。

```typescript:src/index.ts
import { prisma } from './generated/prisma-client'

...

const resolvers = {
    Query: {
        todo: (_: any, { id }: { id: number}) => {
            return prisma.todo({ id });
        },
        todoes: (_: any, { title }: { title: string }) => {
            return prisma.todoes({ where: { title_contains: title }});
        },
    },
};
```

無事に結果が返ってきました！
<img width="1440" alt="スクリーンショット 2019-12-21 1.56.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54838/6696af9d-4d8b-bf68-814c-49aca0152419.png">

## スキーマ定義を別ファイルに切り出す

テンプレートリテラルでスキーマ定義を書いていくのは見づらくメンテナンス性も悪そうなので、別ファイルに分割します。

### graphql-import のインストール
[Urigo/graphql-import](https://github.com/Urigo/graphql-import) はGraphQLのスキーマ定義ファイルの import を可能にしてくれるライブラリです。

```
$ yarn add graphql-import
```

## スキーマ定義を分割
スキーマ定義を配置するディレクトリを新しく作ります。

```
$ mkdir src/schema
```

```graphql:src/schema/todoes.graphql
type Todo {
    id: Int!
    title: String
}
```

```graphql:src/schema/schema.graphql
# import Todo from "todoes.graphql"

type Query {
    todo(id: Int): Todo
    todoes(title: String): [Todo]
}
```

## スキーマ定義をimportする

スッキリしました。

```typescript:src/index.ts
import express from 'express';
import { ApolloServer, gql } from 'apollo-server-express';
import { prisma } from './generated/prisma-client';
import { importSchema } from 'graphql-import';

const typeDefs = importSchema(`${__dirname}/schema/schema.graphql`);

const resolvers = {
    Query: {
        todo: (_: any, { id }: { id: number}) => {
            return prisma.todo({ id });
        },
        todoes: (_: any, { title }: { title: string }) => {
            return prisma.todoes({ where: { title_contains: title }});
        },
    },
};

const server = new ApolloServer( { typeDefs, resolvers });
```

## おわりに
少し触った感じだと、実装の大枠は意外と REST API と変わらない感じがしました。
ただ、本格的に実装を始めたら色々と大きな違いは出てきそうだと思います。

今までモヤモヤしていたGraphQLのサーバー実装の概要が理解できて良かったです。

[prisma-labs/graphqlgen](https://github.com/prisma-labs/graphqlgen) というライブラリを使えばスキーマ定義からリゾルバの自動生成もできたり？と、prisma をちゃんと使えばスキーマ定義を起点にした新しい開発体験が出来ると期待しているので、もっと色々と試してみたいです。