PostgreSQLとPostGraphileでサクッとGraphQLエンドポイントを作成する

ゆめみ その2 Advent Calendar 2019 23日目の記事です。

## はじめに

PostgreSQL のスキーマ情報から、自動的にGraphQLエンドポイントを作成する **PostGraphile** というツールを見つけたので試してみました。

**PostGraphile**
https://www.graphile.org/postgraphile/

## PostGraphile とは

PostGraphile は PostgreSQL のテーブル、列、インデックス、リレーションシップ、View、型、関数、コメントなどを自動的に検出し、GraphQL APIを提供する Node.js 製のオープンソースソフトウェアです(以前は「PostGraphQL」と呼ばれていました)。

基本的な使い方としては PostgreSQL を用意して、コマンドラインから postgraphile コマンドに引数を指定して実行するだけです。機能が足りない場合はプラグインで機能拡張したり、自分で JavaScript (Node.js) のコードを書いて拡張することもできます。

この記事では PostGraphile を実際に動かして query を実行するところまでを紹介します。

## 検証環境

```sh:sh
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.2
BuildVersion:	19C57

$ docker -v
Docker version 19.03.5, build 633a0ea

$ docker-compose -v
docker-compose version 1.24.1, build 4667896b

$ node -v
v12.14.0

$ npm -v
6.13.4
```

## PostgreSQL のセットアップと起動

あらかじめ Docker と docker-compose がインストール済みとします。

まず docker-compose を使って検証用の PostgreSQL (Server) を起動させるための準備をします。

```sh:sh
# 作業用のディレクトリ作成
mkdir -p postgraphile-example && cd $_

# docker-compose.yml ファイル作成
touch docker-compose.yml
```

エディターで `docker-compose.yml` を以下に書き換えます。

```yml:docker-compose.yml
version: '3.5'
services:
  postgres:
    # https://hub.docker.com/_/postgres
    image: postgres:11.6
    volumes:
      - ./initdb.d:/docker-entrypoint-initdb.d:ro
    environment:
      - "POSTGRES_USER=${POSTGRES_USER:-postgres}"
      - "POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-please-change-postgres-password}"
    ports:
      - "15432:5432"
```

次に、何もデータが存在しないと検証がしづらいため、以下のカラムを持ったmemoテーブルを作成します(スキーマはpublic)。
 
**テーブル: "public.memo"**

カラム論理名 | カラム物理名 | 型 + α
-----------|------------|-------
ID (主キー) | id         | serial primary key
タイトル    | title       | text not null default ''
内容        | content    | text not null default ''
作成日時     | created_at | timestamptz default now()
最終更新日時 | updated_at | timestamptz default now()

なお、Docker の postgres イメージには、初回起動時に `/docker-entrypoint-initdb.d` にあるSQLやスクリプトを実行する仕組みがあるので、それを利用します。

```sh:sh
# DB初期化用のディレクトリ作成
mkdir initdb.d

# 初期化SQLファイル作成
touch initdb.d/create_example_db.sql
```

`create_example_db.sql` をエディターで以下のように書き換えます。

```sql:initdb.d/create_example_db.sql
-- データベース作成
create database exampledb encoding 'utf8';

-- 接続データベース切替
\c exampledb;

-- public スキーマのデフォルト権限削除
alter default privileges revoke execute on functions from public;

-- set_update_at 関数作成
create function public.set_updated_at() returns trigger as $$
begin
  new.updated_at := current_timestamp;
  return new;
end;
$$ language plpgsql;


-- memo テーブル作成
create table public.memo (
    id serial primary key
    , title text not null default ''
    , content text not null default ''
    , created_at timestamptz default now()
    , updated_at timestamptz default now()
);

-- updated_at の自動更新トリガー作成
create trigger memo_updated_at before update
  on public.memo
  for each row
  execute procedure public.set_updated_at();


-- memo テーブル初期データ投入
insert into public.memo (title, content)
values
  ('Title 1', 'memo content 1')
  , ('Title 2', 'memo content 2')
  , ('Title 3', 'memo content 3')
;

-- 接続用ユーザー作成 (パスワードは必要に応じて変更してください)
create role example_user login password 'postgres-example-password';

-- example_user に public スキーマの利用権限付与
grant usage on schema public to example_user;

-- example_user に memo テーブルの操作権限付与
grant all on table public.memo to example_user;

-- example_user に memo の primary key シーケンス利用権限付与
grant usage on sequence public.memo_id_seq to example_user;
```

そして、PostgreSQL を起動します。

```bash
# PostgreSQL 起動
docker-compose up -d

# プロセス確認
docker-compose ps
```

PostgreSQL が起動しているのを確認できたら、psql コマンドで接続確認を行います。

```bash
# psql 実行
docker-compose exec postgres psql -U postgres -d exampledb
```

memo テーブルが作成されているか確認します。

```sql
-- psql (11.6 (Debian 11.6-1.pgdg90+1))
-- Type "help" for help.
--
-- exampledb=#
select * from memo;

-- 実行結果
/*
 id |  title  |    content     |          created_at           |          updated_at           
----+---------+----------------+-------------------------------+-------------------------------
  1 | Title 1 | memo content 1 | 2019-12-23 13:37:43.997014+00 | 2019-12-23 13:37:43.997014+00
  2 | Title 2 | memo content 2 | 2019-12-23 13:37:43.997014+00 | 2019-12-23 13:37:43.997014+00
  3 | Title 3 | memo content 3 | 2019-12-23 13:37:43.997014+00 | 2019-12-23 13:37:43.997014+00
(3 rows)
*/

exit;
```

問題なさそうですね。これで PostgreSQL の準備が整いました。

では PostGraphile を動かしてみましょう。

## PostGraphile のセットアップと実行

PostGraphile は node.js で動きます。

npm パッケージとして公開されており、グローバルインストールすることで単体のコマンドとして実行することもできます。

今回は手っ取り早く実行するため、グローバルインストールを利用します。

```sh
# PostGraphile のグローバルインストール
npm install -g postgraphile

# PostGraphile のバージョン確認
postgraphile --version
#=> 4.5.5
```

インストールが完了したら、PostGraphile を起動します。

```sh
DBUSER_PASSWORD="postgres-example-password"
DATABASE_URL="postgres://example_user:${DBUSER_PASSWORD}@localhost:15432/exampledb"

# 開発用設定で PostGraphile を起動
# 参考: https://www.graphile.org/postgraphile/usage-cli/
postgraphile \
  --watch \
  --dynamic-json \
  --no-setof-functions-contain-nulls \
  --no-ignore-rbac \
  --no-ignore-indexes \
  --show-error-stack=json \
  --extended-errors hint,detail,errcode \
  --export-schema-graphql schema.graphql \
  --graphiql "/graphiql" \
  --enhance-graphiql \
  --allow-explain \
  --enable-query-batching \
  --legacy-relations omit \
  --connection $DATABASE_URL \
  --schema public
```

PostGraphile が起動したら、ブラウザで http://localhost:5000/graphiql にアクセスします。

macOSの場合はコマンドラインで以下を実行するとブラウザが起動します。

```sh
# for macOS
open http://localhost:5000/graphiql
```

すると、GraphQL 開発支援ツールの **GraphiQL (グラフィカル)** が表示されます。

![ScreenShot 2019-12-23 23.02.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24450/e8d79ebe-140b-cdd4-a12e-c0b8e86df1e5.png)

左の Explorer を確認すると、PostgreSQL に定義されている memo テーブルの情報から自動的に `allMemos` や `memo` などの query が作成されていることがわかります。

実際にクエリを実行してみましょう。

GraphiQL の真ん中のテキストエリアに以下のクエリを入力します。

```graphql
query MyQuery {
  allMemos {
    nodes {
      id
      title
      content
      createdAt
      updatedAt
    }
  }
}
```

そして画面上部の「▶」をクリックします。

すると memo テーブルに入っているデータが返ってきます。

![ScreenShot 2019-12-23 23.06.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24450/7a5e422d-cf8b-fc7a-836c-16f1cb28b56a.png)

GraphQL の結果が返ってきましたね。

続いて、同じことをターミナルでも試してみましょう。


```sh:ターミナル
curl -X POST -H "Content-Type: application/json" http://localhost:5000/graphql \
     -d '{"query": "query { allMemos { nodes { id title content createdAt updatedAt }}}"}' | jq .
```

```json:実行結果
{
  "data": {
    "allMemos": {
      "nodes": [
        {
          "id": 1,
          "title": "Title 1",
          "content": "memo content 1",
          "createdAt": "2019-12-23T13:37:43.997014+00:00",
          "updatedAt": "2019-12-23T13:37:43.997014+00:00"
        },
        {
          "id": 2,
          "title": "Title 2",
          "content": "memo content 2",
          "createdAt": "2019-12-23T13:37:43.997014+00:00",
          "updatedAt": "2019-12-23T13:37:43.997014+00:00"
        },
        {
          "id": 3,
          "title": "Title 3",
          "content": "memo content 3",
          "createdAt": "2019-12-23T13:37:43.997014+00:00",
          "updatedAt": "2019-12-23T13:37:43.997014+00:00"
        }
      ]
    }
  }
}
```

curl でも動作確認できました！

やったね！(｀・ω・´)

## 所感

PostGraphile を実際に触ってみて、個人的に嬉しいのが以下のポイントです。

* DB定義を更新すると、GraphQLスキーマが自動生成される
    * サーバーアプリケーションの更新が不要
* カスタム関数を定義することで好きな形でレスポンスを返すことができる
    * 必要に応じてカスタムのクエリやミューテーションを定義できる
    * DBの構造が漏れ出すのが嫌な場合はカスタム関数定義で完全コントロール可能
* PostgreSQL のロールと RLS (Row-Level Security) を組み合わせることでユーザー識別ができる
    * デフォルトの認証には JWT が使われる
    * 他の認証方式に対応させるにはプラグインか自分でコードを書くことで対応可能
* LISTEN / NOTIFY または 論理デコードによるリアルタイム機能がある
    * GraphQL の Subscriptions や Live Queries (実験的) に対応
    * 参考: https://www.graphile.org/postgraphile/realtime/

基本的にデータベース(PostgreSQL)にロジックを集約することになるため、GraphQL API経由だけでなく直接SQLで同じロジックを走らせることができるのが大きなメリットとなります（いざとなればSQLだけでも運用可能）。

一方、デメリットとしてはデータベースにロジックを集約しているため、データベースに負荷がかかることと、またデータベース自体のスケール{アップ,アウト}させるのが難しい点があげられます。

不特定多数が利用するサービスで PostGraphile を利用すると運用が大変かもしれません。

まずは個人的なツールや社内向けサービスのバックエンドとして利用するのが良さそうです。

## まとめ

PostGraphile を利用することで、PostgreSQL のスキーマ情報から自動的に GraphQL エンドポイントを作成することができました。

この記事では紹介しておりませんが、PostGraphile は mutaion や subscriptions の機能も提供していたり、また、Express.js のミドルウェアとして稼働させることもできるので必要に応じて機能を拡張させることが可能となっています。

ここでは紹介しきれない機能が盛り沢山なので興味ある方はぜひ PostGraphile の公式サイトをチェックしてみてください。

最後までお読みいただきありがとうございました！

## 参考URL

- PostGraphile | Quick Start Guide  
  https://www.graphile.org/postgraphile/quick-start-guide/
- PostGraphile | PostgreSQL Schema Design - https://www.graphile.org/postgraphile/postgresql-schema-design/