続・Golangを使って簡単なwebバックエンドを書いた (意訳: Prismaを使ったDBスキーマ管理とGolangにおけるセッションハンドリング)


はじめに

どうも。飛び上がり自殺をしようとしたら一般気象学 第2版補訂版(著: 小倉 義光, 2016)を持った奇妙な関西弁を話す謎の生物に止められたため、今度はドリルでモホロビチッチ不連続面まで掘削し、その過程の地熱と地圧で死ねないか考えている者です。

前回、Golangで簡単なWebバックエンドを書いたの記事では、次の2点の問題について取り上げました。

  • 認証周り (セッション管理)がちょーっと弱い。
  • データベースのマイグレーションがDjangoやRailsのように実用性のあるものではない。

今回はこの2点を頑張って対処してみます。

セッション管理の問題

この問題ついては別途、ライブラリを作りました。👉gauth

このライブラリは「ガウス」とか「ゲウス」とか「ギャウス」とか読んであげてください。

gauthはベースとなる技術として、JWTを使っています。JWTについては調べていただくことにして、このライブラリによって、

  • セッション管理
  • ログインが必須なリソースの保護

ができるようになりました。めでたしめでたし

DBの問題

GolangでORMを行う場合、Gormの利用が筆頭に上がるかと思います。が、このライブラリの自動マイグレーションはフィールドの追加とテーブルの追加以外の事ができません。 例えば、gormで次の事をした場合、マイグレーションを自分で書き上げなければなりません。

  • NULL許容だったフィールドをNULL非許容にした
  • フィールドやテーブルをリネームした
  • フィールドを消した
  • すでにあるフィールドにインデックス属性をつけた

Djangoはここら辺が非常によくできていて、相当に特殊な場合を除いて、python manage.py makemigrationsを実行するだけで、これらの問題に対処するマイグレーションファイルをいい感じに書いてくれます。が、Golangではそういったいい感じのDBマイグレーションの生成支援ツールがない。

・・・ よし、作るか

等と普段の僕なら考えるのですが、巷ではSpec-Drivenな開発手法が流行っているようです。Zノーテーション万歳!と言いたいところですが、そこまで高度な形式仕様の記述は必要としていません。欲しいのは何かしらの言語で記述されたデータモデルの仕様をDBのスキーマに落としてくれるツールだ!!しかも実用的なマイグレーションつきで!!

というわけで探したら・・・ ありました!!Prismaが!!

Prismaとは

公式サイトより引用すると、

Prisma replaces traditional ORMs

との事です。つまり、一般的なORMを置き換えることを目的としたツールという事になります。

Prismaの機能

Prismaがもつ機能として、次の機能があります:

  • DBとの調停
  • GraphQLベースのDSLによってモデルの仕様を書くことができる
  • 実用的なマイグレーション (なお2.0ではマイグレーションのバージョニングが追加されるという。)
  • DSLによって記述されたモデルデータをGolangなりTypescriptなりのクライアントとして落とし込める
  • Adminパネル

・・・Djangoが使えない(というか巷ではNodeJSやGolangなどでWebバックエンドを開発することが多いそうですが)場合は間違いなくPrismaはモデル管理ツールの筆頭候補になりそうですね。素晴らしい。

使ってみる

と、いうわけでPrismaを使ってみます。

1. Prismaクライアントのインストール

クライアント自体はNPMに置かれています。と、いうわけで、おもむろにコンソールを開いて次のコマンドでクライアントをインストールします。

npm i -g prisma

2. サーバーのインストール

PrismaのサーバーはDocker化されています。つまり、docker-compose.yml あたりで開発に必要な構成を定義して開発を行うとようにしても良いのですが、今回は単純なコードサンプルだけなので、PostgresPrismaをdockerコンテナとして動かします:

docker-compose.yml
version: '3'

services:
  db:
    image: postgres:alpine
    restart: always
    environment:
      POSTGRES_PASSWORD: go-sample
      POSTGRES_USER: go-sample
  prisma:
    image: prismagraphql/prisma:1.31
    restart: always
    depends_on:
      - db
    ports:
      - "4466:4466"
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: postgres
            host: db
            port: 5432
            user: go-sample
            password: go-sample

今回、使用するサーバーのバージョンは1.31ですが、これより新しいバージョンのアプリがリリースされた場合はそれを使用するようにしてください。

Prismaのサーバーを終了するとSIGKILLが発生する件について

頑張ってPrismaPostgresdocker-compose.ymlを書き、さあやっと遊べますよヤッター⭐と思ったのですが、なんとdocker-composeを終了する時にPrismaが終了コードコード137を返すではありませんか!!

go-gql-sample_prisma_1 exited with code 137 <-- アッー!!
db_1       | 2019-04-28 03:24:13.906 UTC [1] LOG:  received smart shutdown request
db_1       | 2019-04-28 03:24:13.907 UTC [1] LOG:  background worker "logical replication launcher" (PID 24) exited with exit code 1
db_1       | 2019-04-28 03:24:13.907 UTC [19] LOG:  shutting down
db_1       | 2019-04-28 03:24:13.915 UTC [1] LOG:  database system is shut down
go-gql-sample_db_1 exited with code 0

この問題、まさに以前Qiitaで記事にしたことのある問題ドンピシャなのです:

と、いうわけでこの問題に対処します。 まず、原因箇所を特定するため、docker-compose psを実行してPrismaの中でどういったプロセスが実行されているのか調べます:

          Name                         Command              State            Ports         
-------------------------------------------------------------------------------------------
go-gql-sample_db_1          docker-entrypoint.sh postgres   Up       5432/tcp              
go-gql-sample_prisma_1      /bin/sh -c /app/start.sh        Up       0.0.0.0:4466->4466/tcp

この出力では、go-gql-sample_prisma_1Prismaのコンテナになります。そして、このコンテナが実行しているコマンドはどうやらシェルで書かれているようですね。というわけでその中身を見てみましょう。

/app/start.sh
#!/bin/bash
set -e
/app/prerun_hook.sh
/app/bin/prisma-local  # <-- これ!

はいビンゴ!つまり/app/bin/prisma-localは新しく作成されたプロセス内で実行されます。(i.e. SIGINTが伝わってこない) そして同様に/app/bin/prisma-localについてもここに書きたいところですが、当該のスクリプトをここに書くのは長過ぎるので、結論のみを書くと、/app/bin/prisma-localはちゃんとexecを使ってサーバーを起動しておりました。

というわけで、/app/start.shの内容を次のように書き換え、新しくDockerイメージを作成します:

/app/start.sh
#!/bin/sh -e
# -*- coding: utf-8 -*-

/app/prerun_hook.sh
exec /app/bin/prisma-local  # execビルトインコマンドはプロセスを置き換える
prismasvr.dockerfile
FROM prismagraphql/prisma:1.31
COPY ./prisma-patch.sh /app/start.sh
CMD [ "/app/start.sh" ]
docker-compose.yml
version: '3'

services:
  db:
    image: postgres:alpine
    restart: always
    environment:
      POSTGRES_PASSWORD: go-sample
      POSTGRES_USER: go-sample
  prisma:
    build:
      context: ./
      dockerfile: prismasvr.dockerfile
    restart: always
    depends_on:
      - db
    ports:
      - "4466:4466"
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: postgres
            host: db
            port: 5432
            user: go-sample
            password: go-sample

上記の変更を行った後、docker-compose updocker-compose stopを行って様子を見てみましょう。

go-gql-sample_prisma_1 exited with code 143
db_1         | 2019-04-30 06:42:17.810 UTC [1] LOG:  received smart shutdown request
db_1         | 2019-04-30 06:42:17.813 UTC [1] LOG:  background worker "logical replication launcher" (PID 24) exited with exit code 1
db_1         | 2019-04-30 06:42:17.813 UTC [19] LOG:  shutting down
db_1         | 2019-04-30 06:42:17.835 UTC [1] LOG:  database system is shut down
go-gql-sample_db_1 exited with code 0

と、このように、Prismaを正常に終了させるようにする事ができました。

尚、これはバグなのでGithubにプルリクを出しています。マージされるかどうかは不明。

3. prisma.ymlの作成

Prismaには環境設定ファイルが存在し、これを以下のコマンドで簡単に作成しておきます:

prisma init --endpoint http://localhost:4466

このコマンドを実行すると、prisma.ymldatamodel.prismaの2つのファイルが作成されます。前者が環境設定ファイル、後者がデータモデル定義ファイルとなります。また、データモデルの定義には複数のファイルを指定することができ、そのようにする場合はprisma.ymlを次のように書き換える必要があります。

prisma.yml
endpoint: http://prisma:4466
datamodel:
  - models/user.prisma
  - models/payment.prisma
  - models/etc.prisma

しかし、今回の場合、記述するべきモデルは一つだけなので、デフォルトの設定でも問題ありません。

4. データモデルの定義

さて、Prismaのバグに簡易パッチを当て、ようやくデータモデルの定義に移ることができます。先にも述べたように、Prismaのモデル定義のDSLはGraphQLtypeに各種ディレクティブを付け加えたものになっています。

そして、今回の簡単な認証バックエンドに必要なモデルは一つ。ユーザー名とパスワードのハッシュを格納するUserモデルのみです。これを定義します:

datamodel.prisma
type User {
  id: ID! @unique @id
  username: String! @unique
  password: String!
}

上記コードで独特な点は、@unique@idで、次の効果があります。

  • @unique はSQLで言うところのUNIQUE制約、つまり、「保存されているデータの当該カラムの情報はユニークでなければならない」という制約です。
  • @id はIDで型付けされたフィールドのみ有効な語句で、これが指定される事によって当該のフィールドの値はPrismaによって自動生成され、ユーザがAdminパネルで編集することはできません。

これらのディレクティブに加え、Date型には@createdAt等のディレクティブなども用意されています。詳細についてはヘルプをご覧頂くとして、取り敢えず上記コードでUserモデルを定義することができました。

5. モデルの展開

次に、モデルをPrismaに展開させます。とはいえ、やることは単純に次のコマンドを実行するだけです:

prisma deploy

6. クライアントの作成

次に、バックエンドからPrismaを操作するためのクライアントを作成します。今回はGraphQLベースのバックエンドなので、作成すべきコードは次の2つです:

これらのコードを作成するには、prisma.ymlに少し変更を加えます:

prisma.yml
endpoint: http://prisma:4466
datamodel:
  - models/user.prisma
generate: # 👈👇この項目を追加する
  - generator: go-client
    output: ./backend/prisma
  - generator: graphql-schema
    output: ./backend/schemata/generated.graphql
hooks:
  post-deploy:
    - prisma generate # 👈 prisma deployした時に自動的にクライアントも更新させる

上記変更を行った後、prisma deploy あるいは、prisma generateを実行すると、./backend/prismaにGo用クライアントが、./backend/schemata/generated.graphqlGraphQL スキーマが生成されます。

7. ゴリゴリ書く

あとはゴリゴリとバックエンドを書くだけです
また、書いたコードはここにあります。ありがとうございました。

終わりに

とりあえず、Prismaの使い方をざっくり書いてみましたが、コードを修正してプルリク送るよりもQiitaに記事をアウトプットするほうが労力がかかるように思えました。独創的な死に方で自殺する前に考えた事をコードに落とし込むAIと考えたことをブログ記事にするAIはよ!!

では。