Help us understand the problem. What is going on with this article?

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

はじめに

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

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

コンテナ構成

Prisma の公式ページに アーキテクチャが載っています。

Prismaレイヤーアーキテクチャ

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

ローカル.png

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

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

開発環境.png

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

ディレクトリ構成

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

親ディレクトリ
├── 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 のディレクトリ構成とは大幅に異なりますが、これは Docker Compose による操作や指定をわかりやすくするためです。
通常のプロジェクトではちゃんと Golang の仕様や思想に従ったディレクトリ構成をお勧めします。

API サーバーの設定

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

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 ライブラリを利用しましょう。

FROM node:13.1-alpine3.10

WORKDIR /usr/src/app

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

Docker Compose 設定

Prisma サーバーと MySQL については、公式のページにサンプルがあります。これを参考にしつつ書いていきましょう。

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 は起動に時間がかかるため、初回の接続は失敗する
    • 接続失敗するとコンテナが落ちてしまうので、接続できるまで再起動し続けるための設定

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

$ docker-compose up -d

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

DB の操作

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

$ docker-compose exec prisma-cli ash

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

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

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

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

スクリーンショット 2019-11-24 13.51.06.png

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

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

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

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

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

API サーバー

それでは、生成した Prisma Client を使って API サーバーを作成しましょう。
先ほどと同じようにシェルを使って API サーバーに入ってもいいのですが、私は VSCode プラグインのRemote - Containersをお勧めします。
なんでもいいので、シェルが使える状態になったら OK です。
まずは先ほど生成した Prisma Client を使ってデータを取得してみましょう。公式ページのこの辺りこの辺りを参考に進めていきます。

$ cd /go/src/app
$ dep init
$ touch main.go
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 は学習中なのでクオリティは察してください。
とりあえず実行してみましょう。

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

DB に登録した「Alice」「Bob」「Charles」の情報が取得できていますね。ck3e~の文字列は自動生成された ID です。
あとはこれを HTTP リクエストに対して Json 形式でレスポンスできれば OK です。この記事この記事を参考に進めていきましょう。

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です。

$ go run ./main.go
/users
{
  "ck3ez5b7e00110766n4zr6wro": {
    "name": "Alice"
  },
  "ck3ez5b8u001507666qcbs2qy": {
    "name": "Bob"
  },
  "ck3ez5b9b001907668hf3pgyw": {
    "name": "Charles"
  }
}

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

おわりに

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

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

参考資料

Prisma 関連

prisma - 最速 GraphQL Server 実装
Prisma
Prisma Docs
Prisma 公式サーバーイメージ
Prisma2

Docker 関連

いい加減 docker-compose で links を使うのをやめて network でコンテナ間名前解決をする
Dockerfile のベストプラクティス
Compose ファイル・リファレンス

Golang 関連

Go にはディレクトリ構成のスタンダードがあるらしい。
VSCode で Go の Modules 設定
golang で REST API をやってみた ①
Go-Json-Rest
Standard Go Project Layout
Go の構造体にメタ情報を付与するタグの基本
Golang
dep(Golang の依存性管理ツール)

その他

Remote Containers
GraphQL
draw.io(製図に利用)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした