Node.js
TypeScript
GraphQL
prisma
graphql-yoga

Prismaを使ってMySQLに接続するGraphQL APIサーバーを構築

はじめに

基本的にPrismaの公式チュートリアル Build a GraphQL server from Scratch に倣って、Prismagraphql-yogaを使ってGraphQLサーバーを書いていきます。

本記事では上記チュートリアルとは少し異なるアプローチ

  • 言語:TypeScript
  • DB:ローカルに立てるDocker上のMySQL

でご紹介します。

チュートリアルを進める前に、Prismaって何?という方は、

辺りを一読してPrismaをざっくりと理解し、公式の

を確認してみるのがオススメです。

対象読者

  • GraphQLに関する知識をある程度有している方
  • GraphQL APIを提供するサーバーを簡単に素早く作りたい方
  • データベースは自前のMySQLを使いたい方
  • 言語はTypeScriptで書きたい方

実行環境

  • Mac OS Mojave: v10.14.2
  • node: v11.2.0
  • npm: v6.5.0
  • yarn: v1.12.3
  • docker: v17.09.1-ce
  • prisma: v1.23.4
  • graphql: v3.0.4

GraphQL APIサーバー(Backends For Frontends)構築

はじめに、外部に向けてGraphQL APIを提供するためのBFFを構築していきます。
後に、外部には公開しない内部向けのPrismaサーバーをラップするように構築します。

NPMプロジェクト用ディレクトリ作成〜初期化

mkdir prisma-yoga-mysql-typescript-sample
cd prisma-yoga-mysql-typescript-sample
npm init -y

src/index.tsの作成

mkdir src
touch src/index.ts

graphql-yogaのインストール

yarn add graphql-yoga

nodemonts-nodeのインストール

ここでチュートリアルではsrc/index.jsにコードを書き込んで実行しますが、TypeScriptで同じことを実行するために、コンパイル無しでNode.js上で直接TSファイルを実行できるライブラリts-nodeと、ファイルを監視し、変更があればNodeのプロセスを再起動してくれるnodemonをインストールします。

yarn add -D nodemon ts-node

package.jsonのアップデート

yarn startでスクリプトを実行できるように、package.jsonのscriptsの値を次のように書き換えます。

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon --ext ts,yaml,graphql --exec 'ts-node' src/index.ts"
  },

(拡張子がtsyamlgraphqlのファイルを監視対象とする)

src/index.tsへの新規書込

src/index.ts
import { GraphQLServer } from 'graphql-yoga';

// GraphQL SDLに沿ってschemaを定義
const typeDefs = `
  type Query {
    description: String
  }
`;

// GraphQL APIのリクエストに応えるための実装
const resolvers = {
  Query: {
    description: () => `This is the API for a simple blogging application`,
  },
};

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

server.start(() =>
  console.log(`The server is running on http://localhost:4000`),
);

ここまで保存した時点で、yarn startをコマンドラインで実行すると、http://localhost:4000でサーバーが立ち上がるはずです。

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

{
  description
}

スクリーンショット 2019-01-07 3.44.41.png

src/schema.graphqlの作成

続けて、src/index.tsのtypeDefsに代入したものとは別に、GraphQLスキーマをあらためて定義するために、次のようにschema.graphqlを作成します。

touch src/schema.graphql
src/schema.graphql
type Query {
  posts: [Post!]!
  post(id: ID!): Post
  description: String!
}

type Mutation {
  createDraft(title: String!, content: String): Post
  deletePost(id: ID!): Post
  publish(id: ID!): Post
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
}

src/index.tsのアップデート

上記で作成したスキーマを読み込むために、src/index.tsを次のように書き換えます。

src/index.ts
import { GraphQLServer } from 'graphql-yoga';

// GraphQL SDLに沿ってschemaを定義
// const typeDefs = `
//   type Query {
//     description: String
//   }
// `;

// GraphQL APIのリクエストに応えるための実装
const resolvers = {
  Query: {
    description: () => `This is the API for a simple blogging application`,
  },
};

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
});

server.start(() =>
  console.log(`The server is running on http://localhost:4000`),
);

それぞれ保存した後、nodemonによってプロセスが再起動された場合はそのまま http://localhost:4000 にアクセスして、schemaが次のように反映されていればOKです。

スクリーンショット 2019-01-07 4.13.42.png

Prismaサーバー(内部向け)構築

ここからPrismaを使って、内部向けにサーバーを構築していきます。
これから作る内部向けGraphQL APIサーバーは、DBに対して直接クエリを実行します。

Prisma CLIのグローバルインストール

prismaコマンドが未だ使えない場合は、グローバルインストールしてください。

npm install -g prisma

databaseディレクトリの作成

次のprismaコマンドをプロジェクトのルートで実行すると

prisma init database

REPLで実装を選ぶことになりますので、Create new databaseMySQLPrisma TypeScript Clientをそれぞれ選択します。
本家のチュートリアル Step 13とは別の選択を取っていますので、ご注意ください)

 % prisma init database
? Set up a new Prisma server or deploy to an existing server? 

  Set up a new Prisma server for local development (based on docker-compose):
  Use existing database      Connect to existing database 
❯ Create new database        Set up a local database using Docker 

  Or deploy to an existing Prisma server:
  Demo server                Hosted demo environment incl. database (requires login) 
  Use other server           Manually provide endpoint of a running Prisma server

? What kind of database do you want to deploy to? (Use arrow keys)
❯ MySQL             MySQL compliant databases like MySQL or MariaDB 
  PostgreSQL        PostgreSQL database 
  MongoDB           Mongo Database 

? Select the programming language for the generated Prisma client 
❯ Prisma TypeScript Client 
  Prisma Flow Client 
  Prisma JavaScript Client 
  Prisma Go Client 
  Don't generate 

コマンド実行後は、databaseディレクトリが作成され、次のような構成になっているはずです。
(本家チュートリアルではdatabase/datamodel.graphqlが作成されていることになっていますが、prisimaのバージョン1.23.4で実行すると、代わりにdatabase/datamodel.prismaが作成されます)

├── database
│   ├── datamodel.prisma // DBのテーブル構造元となる型定義
│   ├── docker-compose.yml
│   ├── generated
│   │   └── prisma-client
│   │       ├── index.ts
│   │       └── prisma-schema.ts
│   └── prisma.yml // Prismaサーバーの設定ファイル
├── package.json
├── src
    ├── index.ts
    └── schema.graphql

datamodel.prismaのアップデート

本家チュートリアルに沿って、次の内容でdatamodel.prismaを書き換えてください。

database/datamodel.prisma
type Post {
  id: ID! @unique
  title: String!
  content: String!
  published: Boolean! @default(value: "false")
}

上記ではどんな定義になっているか、大体想像がつくかと思いますが、デプロイされるとPostテーブルにマッピングされ、idpublishedまでのフィールドと呼ばれる部分は各カラムにマッピングされます。
!はNot nullを指し、@uniqueは値がテーブルの中で一意なフィールドとなるよう指定しています。
@defaultは、レコードをインサートする際、publishedの値指定が無い場合はfalseが自動的に割り当てられる指定となっています。

ここで登場する@unique@defaultなどのディレクティブと呼ばれるものについて詳しく知りたい方は、GraphQLの公式などをご参照ください。

Dockerコンテナの起動

Prismaをデプロイさせるには、DBに接続できる状態にする必要があります。
コマンドラインで、docker-compose.ymlが存在するdatabaseディレクトリに移動し、docker-compose upを実行し、Dockerコンテナを起動してください。

cd database 
docker-compose up

Prismaのデプロイ

上記でDockerコンテナを起動した後、別のコマンドラインで再度databaseディレクトリに移動し、prisma deployコマンドを実行して、database/prisma.ymlに記載されているendpoint: http://localhost:4466 に対してデプロイさせます。

cd database
prisma deploy

デプロイと同時に、DBに対してmigrationが実行されます。
デプロイ完了後、http://localhost:4466 にアクセスし、DOCSを開くと、Postテーブルに対するCRUDが実装されているのが分かります。

スクリーンショット 2019-01-07 5.53.19.png

PrismaサーバーとBFFとの紐付け

最後に、DBを直接操作するPrismaサーバーと、BFF(外部向けに公開するGraphQL APIサーバー)との紐付けを行い、公開するGraphQL APIを通じてPrismaサーバーのGraphQL APIを実行し、DBに対して操作できるようにします。

Prismaサーバーとの紐付け用設定ファイルの作成

プロジェクトのルートに移動し、.graphqlconfig.ymlを次のように作成します。

touch .graphqlconfig.yml 
graphqlconfig.yml
projects:
  database:
    schemaPath: src/generated/prisma.graphql
    extensions:
      prisma: database/prisma.yml
  app:
    schemaPath: src/schema.graphql
    extensions:
      endpoints:
        default: http://localhost:4000

Prismaサーバーのスキーマファイルの作成

上記Yamlのprojects.database.schemaPathで指定されているパスに、スキーマファイルを作成します。

graphql get-schema

prisma-bindingのインストール

紐付けに必要なライブラリprisma-bindingをインストールします。

yarn add prisma-binding

src/index.tsのアップデート

インストールしたprisma-bindingPrismaクラスを使って、上記で作成したsrc/generated/prisma.graphqlを利用するように、src/index.tsを書き換えます。

src/index.ts
import { GraphQLServer } from 'graphql-yoga';
import { Prisma } from 'prisma-binding';
import { IResolvers } from 'graphql-middleware/dist/types';

const resolvers: IResolvers = {
  Query: {
    posts(parent, args, ctx, info) {
      return ctx.db.query.posts({}, info);
    },
    post(parent, args, ctx, info) {
      return ctx.db.query.post({ where: { id: args.id } }, info);
    },
  },
  Mutation: {
    createDraft(parent, { title, content }, ctx, info) {
      return ctx.db.mutation.createPost(
        {
          data: {
            title,
            content,
          },
        },
        info,
      );
    },
    deletePost(parent, { id }, ctx, info) {
      return ctx.db.mutation.deletePost({ where: { id } }, info);
    },
    publish(parent, { id }, ctx, info) {
      return ctx.db.mutation.updatePost(
        {
          where: { id },
          data: { published: true },
        },
        info,
      );
    },
  },
};

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
  context: req => ({
    ...req,
    db: new Prisma({
      typeDefs: 'src/generated/prisma.graphql', // the generated Prisma DB schema
      endpoint: 'http://localhost:4466', // the endpoint of the Prisma DB service
      // secret: "mysecret123", // specified in database/prisma.yml
      debug: true, // log all GraphQL queries & mutations
    }),
  }),
});

server.start(() => console.log('Server is running on http://localhost:4000'));

src/schema.graphqlのアップデート

src/schema.graphqlについても、src/generated/prisma.graphqlで定義されているPostの型を利用できるよう、次のように書き換えます。

src/schema.graphql
# import Post from "./generated/prisma.graphql"

type Query {
  posts: [Post!]!
  post(id: ID!): Post
  description: String!
}

type Mutation {
  createDraft(title: String!, content: String): Post
  deletePost(id: ID!): Post
  publish(id: ID!): Post
}

さいごに

結構なボリュームのチュートリアルでしたが、手を動かして使ってみると慣れてきて、Prismaの有り難さが分かります。
下記の記事によると、Prismaを使えばN+1問題も解決してくれるそうなので、実戦でも使えるようにしていきたいです。
Do Prisma resolve N+1 problem?

ソースコード

今回作成したサンプルは下記のGitHubから手に入ります。
https://github.com/galoi/prisma-yoga-mysql-typescript-sample

その他、参考にさせていただいた記事