Prisma.io + Go のチュートリアル

# Prisma.ioとは？

<img width="148" alt="prisma.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/77de2a04-2ffb-7d77-19cf-0b2be7ed4294.png">

- SQLサーバにGraphQLを生やします。
- 今の所、MySQL、PostgreSQL、MongoDBに対応。
- PrismaサーバはDockerコンテナとして起動。
- GraphQLなので、クライアントはHTTPが使えればPrismaサーバを操作できる。
- Prismaサーバの操作をしやすくするPrismaクライアント(GO, TypeScript, JavaScript)を自動生成できる。
- Prisma Adminでブラウザからデータベースの照会、更新などができる。

雑なまとめなので、[公式サイト](https://www.prisma.io/)を見てもらったほうがいいと思います。

また、以下の記事がすごく参考になりました。ありがとうございます。

[prisma - 最速 GraphQL Server実装 - Qiita](https://qiita.com/mizchi/items/74fb8b1177e83fe10273)
[Prisma.ioでGraphQL APIサーバーを楽して作る - Qiita](https://qiita.com/wawoon/items/6d51703f648533bdbfc8)

# 構成
PrismaサーバはSQLサーバのCRUD全てが出来てしまうので、そのまま公開するのは危険。
なので、アプリケーション/APIサーバの層を追加します。

この層はPrismaクライアントを使って自前で作るので、APIはGraphQLでなくてもOK。
(REST,gRPCとか)

![SUH6AqW.png](https://qiita-image-store.s3.amazonaws.com/0/189588/b6c257ad-e1db-2c31-84df-e29bb31bb809.png)

- Database
  MySQL。Dockerで構築。`prisma init`で自動的に作ってくれる。

- Data Access Layer(Prisma)
  Dockerで構築。`prisma init`で自動的に作ってくれる。

- Application / API Service
  今回はGoで作成。 GraphQLサーバのフレームワークは`gqlgen`を使用。
  最初はホストで直接起動。後からDocker化。

- Client(ブラウザ)
  `gqlgen`で`GraphQL Playground`を追加できるので、そこから操作。
  自分は`GraphiQL`も使ってます。

基本的に公式の[チュートリアル](https://www.prisma.io/docs/get-started/01-setting-up-prisma-new-database-GO-g002/)に沿って進めますが、所々アレンジ入れてます。

完成後のソースは[こちら](https://github.com/otanu/prisma-hello-world)です。

# 環境一覧

試した時の環境です。
環境やバージョンが違っても動くとは思います。

- macOS Mojave
- Docker version 18.09.2, build 6247962
- docker-compose version 1.21.2, build a133471
- Node.js v8.15.1 ※prismaインストールのため

  公式ではbrewも書いてありますが、途中で以下のエラーに遭遇したので、
  npmで入れ直しました。
 
```sh
brew tap prisma/prisma
brew install prisma

Error: Cannot find module 'generate'
```

# Step1 Set up Prisma

## Prisma インストール

```sh
$ npm install -g prisma
$ prisma -v
prisma/1.30.0 (darwin-x64) node-v8.15.1

```

## Prisma init

構築は`GOHOME`のディレクトリ下で構築します。（例: ~/go/src/prisma-hello-world）

まず、prismaコマンドで土台を作ります。
途中質問がくるので、以下を選択。

- Create new database
- MySQL
- Go

```sh
$ cd ~/go/src
$ prisma init prisma-hello-world

? Set up a new Prisma server or deploy to an existing server? Create new database
? What kind of database do you want to deploy to? MySQL
? Select the programming language for the generated Prisma client Prisma Go Client

Created 3 new files:                                                                          

  prisma.yml           Prisma service definition
  datamodel.prisma    GraphQL SDL-based datamodel (foundation for database)
  docker-compose.yml   Docker configuration file

Next steps:

  1. Open folder: cd prisma-hello-world
  2. Start your Prisma server: docker-compose up -d
  3. Deploy your Prisma service: prisma deploy
  4. Read more about Prisma server:
     http://bit.ly/prisma-server-overview
```

実行が終わるとファイルがいくつか作成されています。

まず、DBサーバとPrismaサーバを起動するdocker-composeファイル。

```yaml:docker-compose.yml
version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.30
    restart: always
    ports:
    - "4466:4466"
    environment:
      PRISMA_CONFIG: |
        port: 4466
        # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
        # managementApiSecret: my-secret
        databases:
          default:
            connector: mysql
            host: mysql
            user: root
            password: prisma
            rawAccess: true
            port: 3306
            migrations: true
  mysql:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql
volumes:
  mysql:

```

データモデル定義のパスや、Prismaクライアントの出力先とかの設定ファイル。

```yaml:prisma.yml
endpoint: http://localhost:4466
datamodel: datamodel.prisma

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

データモデル定義。

```graphql:datamodel.prisma
type User {
  id: ID! @unique
  name: String!
}
```

## 起動・デプロイ

まずは初期状態で起動してみます。

```sh
$ docker-compose up -d
$ prisma deploy
```

以下にアクセスすると、PrismaサーバのGraphQL Playgroundが開きます。

`http://localhost:4466`
<img width="1218" alt="prisma_playground.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/97d4a550-3a19-abc1-6731-f98d02f967b3.png">

以下のアドレスにアクセスすると、データ管理が出来るPrisma Adminが開きます。

`http://localhost:4466/_admin`
<img width="1218" alt="prisma_admin.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/cbfe97a3-6025-b2ee-ce43-838c576003fe.png">

# Goクライアントを作成

データが空っぽなので、Goクライアントを実装してデータを追加してみます。

まず、GO MODULESを初期化します。

```sh
$ export GO111MODLUE=on
$ go init
```

Prismaクライアントを使ってデータを登録するソースを作成します。

```go:index.go
package main

import (
	"context"
	"fmt"
	prisma "prisma-hello-world/generated/prisma-client"
)

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

	// Create a new user
	name := "Alice"
	newUser, err := client.CreateUser(prisma.UserCreateInput{
		Name: name,
	}).Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Created new user: %+v\n", newUser)

	users, err := client.Users(nil).Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", users)
}
```
実行すると、１件データが登録されます。
IDは[cuid](https://github.com/prisma/cuid-java)に基づいて自動的に振られます。

```sh
$ go run index.go
Created new user: &{ID:cjuc0tk8f001l07165y3waxtt Name:Alice}
[{ID:cjuc0tk8f001l07165y3waxtt Name:Alice}]
```

# Step2 データモデルの変更

データモデルに項目を追加します。

```graphql:datamodel.prisma
type User {
  id: ID! @unique
  email: String @unique
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID! @unique
  title: String!
  published: Boolean! @default(value: "false")
  author: User
}
```

デプロイとPrismaクライアントの更新をします。

```sh
$ prisma deploy
$ prisma generate
```

`generated/prisma-client/prisma.go`に新しいAPIが追加されましたので、これを使ってデータを登録するソースを作成します。

```go:index.go
package main

import (
	"context"
	"fmt"
	prisma "prisma-hello-world/generated/prisma-client"
)

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

	// Create a new user with two posts
	name := "Bob"
	email := "bob@prisma.io"
	title1 := "Join us for GraphQL Conf in 2019"
	title2 := "Subscribe to GraphQL Weekly for GraphQL news"
	newUser, err := client.CreateUser(prisma.UserCreateInput{
		Name:  name,
		Email: &email,
		Posts: &prisma.PostCreateManyWithoutAuthorInput{
			Create: []prisma.PostCreateWithoutAuthorInput{
				prisma.PostCreateWithoutAuthorInput{
					Title: title1,
				},
				prisma.PostCreateWithoutAuthorInput{
					Title: title2,
				},
			},
		},
	}).Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Created new user: %+v\n", newUser)

	allUsers, err := client.Users(nil).Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", allUsers)

	allPosts, err := client.Posts(nil).Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", allPosts)
}
```
実行すると、新しいユーザ`Bob`と記事が２件追加されます。

```sh
$ go run index.go
Created new user: &{ID:cjuc1dwo1002207164z8feea9 Email:0xc000093520 Name:Bob}
[{ID:cjuc0tk8f001l07165y3waxtt Email:<nil> Name:Alice} {ID:cjuc1dwo1002207164z8feea9 Email:0xc000146320 Name:Bob}]
[{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]
```

次に、登録した記事をemailを指定して検索してみます。

```go:index.go
package main

import (
	"context"
	"fmt"
	prisma "prisma-hello-world/generated/prisma-client"
)

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

	email := "bob@prisma.io"
	postsByUser, err := client.User(prisma.UserWhereUniqueInput{
		Email: &email,
	}).Posts(nil).Exec(ctx)

	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", postsByUser)
}
```

```sh
$ go run index.go 
[{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]
```

# Step3 Build an App

次に、公開用のGraphQLサーバを作っていきます。

まず、gqlgenのパッケージを登録し、初期構築コマンドを入力します。

```sh
$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init
```

実行すると以下のファイルが出来ます。

- gqlgen.yml
  gqlgenの設定。自動生成コードの出力先とかを設定。

- schema.graphql
  公開するGraphQLのスキーマ。この定義からコードが自動生成される。

- generated.go
  gqlgenで自動生成されるコード。
  自動生成するので、一旦削除。

- models_gen.go
  prisma-clientで作成された構造体を使うので不要。削除。

- resolver.go
  GraphQLのリゾルバ。自分で作る必要があるが、必要な関数などのテンプレートは自動生成してくれる。
  自動生成するので、一旦削除。

- server/server.go
  GraphQLサーバ起動のコード

ごちゃごちゃして来たので、フォルダを作って整理します。

- gqlgen/
  - gqlgen.yml
  - schema.graphql
- server/server.go

次に、`gqlgen`の設定をPrismaに合わせて書き換えます。

```yaml:gqlgen.yml
schema: schema.graphql
exec:
  filename: generated.go
models:
  Post:
    model: prisma-hello-world/generated/prisma-client.Post
  User:
    model: prisma-hello-world/generated/prisma-client.User
resolver:
  filename: resolver.go
  type: Resolver
```

公開するGraphQLのスキーマを作成します。

```graphql:schema.graphql
type Query {
  publishedPosts: [Post!]!
  post(postId: ID!): Post
  postsByUser(userId: ID!): [Post!]!
}

type Mutation {
  createUser(name: String!): User
  createDraft(title: String!, userId: ID!): Post
  publish(postId: ID!): Post
}

type User {
  id: ID!
  email: String
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  published: Boolean!
  author: User
}
```

ファイルが出来たら、`gqlgen`を実行してソースを自動生成します。

```sh
$ cd gqlgen
$ go run github.com/99designs/gqlgen
```

以下のファイルが出来ました。

- gqlgen/
  - generated.go
  - resolver.go

自動生成されたGraphQLのリゾルバは枠しかないので、Prismaクライアントを使って実装していきます。

```go:gqlgen/resolver.go
//go:generate go run github.com/99designs/gqlgen
package gqlgen

import (
	"context"
	"prisma-hello-world/generated/prisma-client"
)

type Resolver struct {
	Prisma *prisma.Client
}

func (r *Resolver) Mutation() MutationResolver {
	return &mutationResolver{r}
}
func (r *Resolver) Post() PostResolver {
	return &postResolver{r}
}
func (r *Resolver) Query() QueryResolver {
	return &queryResolver{r}
}
func (r *Resolver) User() UserResolver {
	return &userResolver{r}
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateUser(ctx context.Context, name string) (*prisma.User, error) {
	return r.Prisma.CreateUser(prisma.UserCreateInput{
		Name: name,
	}).Exec(ctx)
}
func (r *mutationResolver) CreateDraft(ctx context.Context, title string, userId string) (*prisma.Post, error) {
	return r.Prisma.CreatePost(prisma.PostCreateInput{
		Title: title,
		Author: &prisma.UserCreateOneWithoutPostsInput{
			Connect: &prisma.UserWhereUniqueInput{ID: &userId},
		},
	}).Exec(ctx)
}
func (r *mutationResolver) Publish(ctx context.Context, postId string) (*prisma.Post, error) {
	published := true
	return r.Prisma.UpdatePost(prisma.PostUpdateParams{
		Where: prisma.PostWhereUniqueInput{ID: &postId},
		Data:  prisma.PostUpdateInput{Published: &published},
	}).Exec(ctx)
}

type postResolver struct{ *Resolver }

func (r *postResolver) Author(ctx context.Context, obj *prisma.Post) (*prisma.User, error) {
	return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &obj.ID}).Author().Exec(ctx)
}

type queryResolver struct{ *Resolver }

func (r *queryResolver) PublishedPosts(ctx context.Context) ([]prisma.Post, error) {
	published := true
	return r.Prisma.Posts(&prisma.PostsParams{
		Where: &prisma.PostWhereInput{Published: &published},
	}).Exec(ctx)
}
func (r *queryResolver) Post(ctx context.Context, postId string) (*prisma.Post, error) {
	return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &postId}).Exec(ctx)
}
func (r *queryResolver) PostsByUser(ctx context.Context, userId string) ([]prisma.Post, error) {
	return r.Prisma.Posts(&prisma.PostsParams{
		Where: &prisma.PostWhereInput{
			Author: &prisma.UserWhereInput{
				ID: &userId,
			}},
	}).Exec(ctx)
}

type userResolver struct{ *Resolver }

func (r *userResolver) Posts(ctx context.Context, obj *prisma.User) ([]prisma.Post, error) {
	return r.Prisma.User(prisma.UserWhereUniqueInput{ID: &obj.ID}).Posts(nil).Exec(ctx)
}
```

先頭の`//go:generate go run github.com/99designs/gqlgen`は、`go generate`コマンドでgqlgenを実行するためのコメントです。
スキーマを修正したら、以下のコマンドでコードを更新できます。

```:例
$ go generate gqlgen/resolver.go
```

次に、アプリ起動部分を作成します。

```go:server/server.go
package main

import (
	"log"
	"net/http"
	"os"
	prisma "prisma-hello-world/generated/prisma-client"
	"prisma-hello-world/gqlgen"

	"github.com/99designs/gqlgen/handler"
)

const defaultPort = "4000"

func main() {
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = defaultPort
	}

	client := prisma.New(nil)
	resolver := gqlgen.Resolver{
		Prisma: client,
	}

	http.Handle("/", handler.Playground("GraphQL Playground", "/query"))
	http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema(
		gqlgen.Config{Resolvers: &resolver})))

	log.Printf("Server is running on http://localhost:%s", port)
	err := http.ListenAndServe(":"+port, nil)
	if err != nil {
		log.Fatal(err)
	}
}
```

できたら、起動してみます。

```sh
$ go run server/server.go
```

以下にアクセスすると、GraphQL Playgroundが開きます。

http://localhost:4000/
<img width="1343" alt="gql-app.png" src="https://qiita-image-store.s3.amazonaws.com/0/189588/5190a086-7787-d839-cd84-357174626149.png">

ためしに、ユーザや記事を追加してみます。

```json:query-ユーザ追加
mutation {
	createUser(name: "otanu") {
    id
    name
  }  
}
```

```json:結果
{
  "data": {
    "createUser": {
      "id": "cjuc3vysh000d0744f8n94vw4",
      "name": "otanu"
    }
  }
}
```

```json:query-記事追加
mutation {
	createDraft(title: "テスト", userId: "cjuc3vysh000d0744f8n94vw4") {
    id
    title
    published
    author {
      id
      name
    }
  }
}
```

```json:結果
{
  "data": {
    "createDraft": {
      "id": "cjuc42km0000j07441ucjddnd",
      "title": "テスト",
      "published": false,
      "author": {
        "id": "cjuc3vysh000d0744f8n94vw4",
        "name": "otanu"
      }
    }
  }
}
```

```json:query-公開
mutation {
	publish(postId: "cjuc42km0000j07441ucjddnd") {
    id
    title
    published
    author {
      name
    }
  }
}
```

```json:結果
{
  "data": {
    "publish": {
      "id": "cjuc42km0000j07441ucjddnd",
      "title": "テスト",
      "published": true,
      "author": {
        "name": "otanu"
      }
    }
  }
}
```

```json:query-公開記事検索
query {
  publishedPosts {
    id
    title
  }
}
```

```json:結果
{
  "data": {
    "publishedPosts": [
      {
        "id": "cjuc42km0000j07441ucjddnd",
        "title": "テスト"
      }
    ]
  }
}
```

# アプリのDocker化

アプリもDockerComposeでまとめて起動できるように、Docker化していきます。

アプリをDocker化すると、prismaサーバへの接続がlocalhostでは繋がらななくなるので、環境変数`ENDPOINT`を追加して、エンドポイントを変更できるようにします。

```go:server/server.go
package main

import (
	"log"
	"net/http"
	"os"
	prisma "prisma-hello-world/generated/prisma-client"
	"prisma-hello-world/gqlgen"

	"github.com/99designs/gqlgen/handler"
)

const defaultPort = "4000"

func main() {
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = defaultPort
	}

  // 追加
	var opt *prisma.Options
	endpoint := os.Getenv("ENDPOINT")
	if len(endpoint) != 0 {
		opt = &prisma.Options{
			Endpoint: endpoint,
		}
	}

	client := prisma.New(opt)
	resolver := gqlgen.Resolver{
		Prisma: client,
	}

	http.Handle("/", handler.Playground("GraphQL Playground", "/query"))
	http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema(
		gqlgen.Config{Resolvers: &resolver})))

	log.Printf("Server is running on http://localhost:%s", port)
	err := http.ListenAndServe(":"+port, nil)
	if err != nil {
		log.Fatal(err)
	}
}
```

次にDockerfileを準備します。
ついでに`fresh`でホットリロードも追加。

```Dockerfile:Dockerfile
FROM golang:1.11-alpine AS build_base
RUN apk add bash ca-certificates git gcc g++ libc-dev

WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
RUN go get github.com/pilu/fresh

COPY . .
EXPOSE 4000
CMD cd server; fresh server.go
```

DockerComposeにアプリの設定を追加。
これで、まとめて起動できるようになりました。

```yaml:docker-compose.yml
version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.30
    restart: always
    ports:
    - "4466:4466"
    environment:
      PRISMA_CONFIG: |
        port: 4466
        # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
        # managementApiSecret: my-secret
        databases:
          default:
            connector: mysql
            host: mysql
            user: root
            password: prisma
            rawAccess: true
            port: 3306
            migrations: true
  mysql:
    image: mysql:5.7
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "4000:4000"
    volumes:
      - .:/app
    depends_on:
      - prisma
    environment:
      ENDPOINT: http://prisma:4466
volumes:
  mysql:
```