Golang✖️Prisma✖️MySQL 開発環境を Docker Compose で構築！

# はじめに

API の個人開発を進めている中で、~~Golang の勉強をサボりつつ~~なんかいい感じの DB マイグレーションツールが欲しいなと調べていたところ、[Prisma](https://github.com/prisma/prisma)というものを見つけました。
GraphQL を利用して DB マイグレーションを行ったり、一部の言語で型安全な ORM として利用できるそうです。

細かい説明は他の記事に譲るとして、ここでは開発環境の構築を行います。
ただし、ローカルはあんまり汚したくないので、できるだけ仮想環境でやりましょう。

## コンテナ構成

[Prisma の公式ページ](https://www.prisma.io/docs)に アーキテクチャが載っています。

![Prismaレイヤーアーキテクチャ](https://i.imgur.com/SUH6AqW.png)

クライアントは API サーバーとやりとりを行い、API サーバーと DB の間に一枚 Prisma を挟む形ですね。
これをローカル環境で再現しようとすると、以下の図の形になるかと思います。

![ローカル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213374/5e6cb213-7688-9cbf-b1e5-f805f42fc2f2.png)

Docker Compose で API サーバー、Prisma サーバー、DB の３マシンを起動し、ブラウザからは API サーバーにのみアクセスする形になります。
ブラウザからは Prisma サーバーおよび DB には直接アクセスできないわけですね。

今回構築する開発環境では、より作業を行いやすいようにもっとゆるく作ります。
具体的には下図の通りです。

![開発環境.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213374/ff327416-cde9-6793-cef9-2e145fcd46f4.png)

最初の図から、Prisma Cli が増えています。また、Prisma サーバーにアクセスできるようになっていますね。
詳細は後述しますが、Prisma サーバーにはブラウザから DB 操作を行うことができるツールが付属しています。
本番環境でこのツールを使えるようにするのは当然 NG ですが、開発環境では何かと便利なので、ポートを解放して使えるようにします。

## ディレクトリ構成

最終的なディレクトリ構成はこんな感じです。

```tree
親ディレクトリ
├── api-server
│   └── dockerfile
├── app
│   ├── Gopkg.lock
│   ├── Gopkg.toml
│   ├── datamodel.prisma
│   ├── generated
│   │   └── prisma-client
│   │       └── prisma.go
│   ├── main.go
│   ├── prisma.yml
│   └── vendor
├── docker-compose.yml
└── prisma-cli
    └── dockerfile
```

[一般的な Golang のディレクトリ構成](https://github.com/golang-standards/project-layout)とは大幅に異なりますが、これは Docker Compose による操作や指定をわかりやすくするためです。
通常のプロジェクトではちゃんと Golang の仕様や思想に従ったディレクトリ構成をお勧めします。

## API サーバーの設定

特筆することはないです。
今回は Golang を使って API サーバーを建てる予定なので、依存性管理ができるように[dep](https://github.com/golang/dep)を入れておきましょう。

```dockerfile
FROM golang:1.13-alpine3.10

WORKDIR /go/src/app

RUN apk update \
  && apk add git \
  && go get -u github.com/golang/dep/cmd/dep
```

## Prisma Cli の設定

Prisma には Cli が公式で用意されています。
Homebrew で公開されているものもありますが、今回は npm ライブラリを利用しましょう。

```dockerfile
FROM node:13.1-alpine3.10

WORKDIR /usr/src/app

RUN npm install -g yarn \
  && yarn global add prisma
```

## Docker Compose 設定

[Prisma サーバーと MySQL については、公式のページにサンプルがあります](https://www.prisma.io/docs/get-started/01-setting-up-prisma-new-database-JAVASCRIPT-a002/)。これを参考にしつつ書いていきましょう。

```yaml
version: "3"

services:
  api-server:
    build: ./api-server
    container_name: api-server
    tty: true
    ports:
      - "8080:8080"
    volumes:
      - ./app:/go/src/app

  prisma-cli:
    build: ./prisma-cli
    container_name: prisma-cli
    tty: true
    volumes:
      - ./app:/usr/src/app

  prisma-server:
    image: prismagraphql/prisma:1.34
    container_name: prisma-server
    restart: always
    ports:
      - "4466:4466"
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: mysql
            host: database
            port: 3306
            user: root
            password: prisma

  database:
    image: mysql:5.7
    container_name: database
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql

volumes:
  mysql: ~
```

いくつか注意点があります。

- API サーバーと Prisma Cli に`tty: true`をつける
  - API サーバーと Prisma Cli は実行し続けるコマンドを持たないため、起動完了後に停止してしまう
  - 停止したコンテナにはアクセスできないので、停止せずにアクセスできる状態を保つための設定
- Prisma サーバーに`restart: always`をつける
  - Prisma サーバーは起動すると DB への接続を試みる
  - MySQL は起動に時間がかかるため、初回の接続は失敗する
  - 接続失敗するとコンテナが落ちてしまうので、接続できるまで再起動し続けるための設定

設定ができたらルートディレクトリに移動し、起動しましょう。

```terminal
$ docker-compose up -d
```

起動処理のログは`$ docker-compose logs`で確認できます。ログから Prisma サーバーの起動を確認できたら、ブラウザで`localhost:4466`にアクセスしてみましょう。アクセスできれば OK です。

## DB の操作

では、構築した環境で DB を操作してみましょう。
まずは Prisma Cli に入ります。ディストリビューションは Alpine なので、bash の代わりに ash を使います。

```terminal
$ docker-compose exec prisma-cli ash
```

次にプロジェクトおよび DB の初期化を行います。

```terminal
$ prisma init --endpoint http://prisma-server:4466
$ prisma deploy
```

ここで、エンドポイントとして`http://prisma-server:4466`を指定しています。
`prisma-server`は docker-compose.yml で指定した Prisma サーバーのサービス名ですね。[Docker Compose では、サービス名を利用してサービス間で通信を行うことができます](https://qiita.com/dyoshikawa/items/05d627b962da35f7d5b6)。つまり、ここでは「Prisma サーバーの 4466 ポート」をエンドポイントとして指定しているのです。
`prisma deploy`は `datamodel.prisma`に従って DB のマイグレーションを行います。この辺りの書き方は[GraphQL](https://graphql.org/learn/)に従っているようなので、そちらを参考にしてください。

初期化が終わったら、データ構造の確認をしてみましょう。ブラウザから`localhost:4466/_admin`にアクセスしてみてください。`datamodel.prisma`の通りにデータ構造が作成できていれば OK です。

<img width="1395" alt="スクリーンショット 2019-11-24 13.51.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213374/414ffd5a-d8a7-93cf-edea-9299c4684e40.png">

このページからは DB の操作を行うことができます。[Prisma Admin](https://www.prisma.io/admin)とか言うらしいです。
試しにいくつかデータを登録してみましょう。画面右中央近くの`+`をクリックし、画面右側に出てくるタブに値を入力するだけ。最後に画面右下のボタンで保存すれば完了です。

最後に Golang 用の Prisma Client を生成して終わりましょう。~~Prisma Cli から Prisma Client を生成ってギャグかな？~~
`prisma.yml`に Golang 用の Prisma Client 生成設定を追加して、生成コマンドを実行します。

```yaml:prisma.yml
endpoint: http://prisma-server:4466
datamodel: datamodel.prisma

generate:
  - generator: go-client
    output: ./generated/prisma-client/
```

```terminal
$ prisma generate
```

うまくいけば、上記の output で指定したディレクトリにファイルが生成されているはずです。

## API サーバー

それでは、生成した Prisma Client を使って API サーバーを作成しましょう。
先ほどと同じようにシェルを使って API サーバーに入ってもいいのですが、私は VSCode プラグインの[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)をお勧めします。
なんでもいいので、シェルが使える状態になったら OK です。
まずは先ほど生成した Prisma Client を使ってデータを取得してみましょう。公式ページの[この辺り](https://www.prisma.io/docs/get-started/01-setting-up-prisma-new-database-GO-g002/#read-and-write-data-using-the-prisma-client)や[この辺り](https://www.prisma.io/docs/prisma-client/)を参考に進めていきます。

```terminal
$ cd /go/src/app
$ dep init
$ touch main.go
```

```golang:main.go
package main

import (
	"app/generated/prisma-client"
	"context"
	"fmt"
)

func main() {
	client := prisma.New(nil)
	ctx := context.TODO()

	users, err := client.Users(nil).Exec(ctx)
	if err != nil {
		panic(err)
	}

	fmt.Println(users)
}
```

はいできました。~~Golang は学習中なのでクオリティは察してください。~~
とりあえず実行してみましょう。

```terminal
$ go run main.go
[{ck3ez5b7e00110766n4zr6wro Alice} {ck3ez5b8u001507666qcbs2qy Bob} {ck3ez5b9b001907668hf3pgyw Charles}]
```

DB に登録した「Alice」「Bob」「Charles」の情報が取得できていますね。`ck3e~`の文字列は自動生成された ID です。
あとはこれを HTTP リクエストに対して Json 形式でレスポンスできれば OK です。[この記事](https://qiita.com/katekichi/items/d94e078b376151858ca4)と[この記事](https://qiita.com/itkr/items/9b4e8d8c6d574137443c)を参考に進めていきましょう。

```golang:main.go
package main

import (
	"app/generated/prisma-client"
	"context"
	"log"
	"net/http"
	"reflect"

	"github.com/ant0ine/go-json-rest/rest"
)

var ctx = context.TODO()
var client = prisma.New(nil)

func main() {
	api := rest.NewApi()
	api.Use(rest.DefaultDevStack...)

	router, err := rest.MakeRouter(
		rest.Get("/users", GetUsers),
	)
	if err != nil {
		log.Fatal(err)
		panic("Failed to setup router.")
	}

	api.SetApp(router)
	log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

func GetUsers(w rest.ResponseWriter, r *rest.Request) {
	users, err := client.Users(nil).Exec(ctx)
	if err != nil {
		handleError(w, err)
		return
	}

	usersMap := usersToMap(users)
	w.WriteJson(usersMap)
}

func handleError(w rest.ResponseWriter, err error) {
	log.Fatal(err)

	result := map[string]string{"error": "server error"}

	w.WriteJson(result)
	w.WriteHeader(http.StatusInternalServerError)
}

func usersToMap(users []prisma.User) map[string]map[string]string {
	result := make(map[string]map[string]string)

	for _, user := range users {
		tag := reflect.TypeOf(user).Field(1).Tag.Get("json")
		result[user.ID] = map[string]string{tag: user.Name}
	}

	return result
}
```

出来上がったら、実行してアクセスしてみましょう。アクセス先は`localhost:8080/users`です。

```terminal
$ go run ./main.go
```

```json:/users
{
  "ck3ez5b7e00110766n4zr6wro": {
    "name": "Alice"
  },
  "ck3ez5b8u001507666qcbs2qy": {
    "name": "Bob"
  },
  "ck3ez5b9b001907668hf3pgyw": {
    "name": "Charles"
  }
}
```

こんな感じの Json レスポンスが取得できれば OK です!

## おわりに

というわけで、仮想環境に Prisma サーバーを軸にした開発環境を構築できました。これを発展させていけば、簡単な API サーバー程度ならすぐ実装できるんじゃないでしょうか。
とはいえ GraphQL の詳細や Prisma Client の利用方法についてはほとんど触れていませんし、ちゃんと運用するためにはもっと勉強が必要でしょうね。覚えることはどんどん増えていきます。

今回は無印の Prisma を利用しましたが、今後リリース予定の [Prisma2](https://github.com/prisma/prisma2) が現在開発されています。
まだ Golang 非対応(2019/11/30)だったので今回は利用しませんでしたが、早いところ Golang で使えるようになってほしいですね。

## 参考資料

### Prisma 関連

[prisma - 最速 GraphQL Server 実装](https://qiita.com/mizchi/items/74fb8b1177e83fe10273)
[Prisma](https://www.prisma.io/)
[Prisma Docs](https://www.prisma.io/docs/prisma-cli-and-configuration/)
[Prisma 公式サーバーイメージ](https://hub.docker.com/r/prismagraphql/prisma/)
[Prisma2](https://github.com/prisma/prisma2)

### Docker 関連

[いい加減 docker-compose で links を使うのをやめて network でコンテナ間名前解決をする](https://qiita.com/dyoshikawa/items/05d627b962da35f7d5b6)
[Dockerfile のベストプラクティス](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)
[Compose ファイル・リファレンス](http://docs.docker.jp/compose/compose-file.html)

### Golang 関連

[Go にはディレクトリ構成のスタンダードがあるらしい。](https://qiita.com/sueken/items/87093e5941bfbc09bea8)
[VSCode で Go の Modules 設定](https://qiita.com/msmsny/items/a8d4573d03774815a198)
[golang で REST API をやってみた ①](https://qiita.com/katekichi/items/d94e078b376151858ca4)
[Go-Json-Rest](https://github.com/ant0ine/go-json-rest)
[Standard Go Project Layout](https://github.com/golang-standards/project-layout)
[Go の構造体にメタ情報を付与するタグの基本](https://qiita.com/itkr/items/9b4e8d8c6d574137443c)
[Golang](https://golang.org/)
[dep(Golang の依存性管理ツール)](https://github.com/golang/dep)

### その他

[Remote Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
[GraphQL](https://graphql.org/)
[draw.io(製図に利用)](https://www.draw.io/)