MyDocs

# prisma-client-goを試した

prisma のセットアップ

まずは project の作成から。

$ go mod init backend

client のインストール。

$ go get github.com/prisma/prisma-client-go

スキーマの作成。

$ npx prisma init

prisma/schema.prisma.env (と .gitignore ) が生成される。

.env ファイルは DATABASE_URL を修正。

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="mysql://app:password@localhost:3306/app"

prisma/schema.prismaclientprisma-client-go に、 datasourcemysql に修正。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "go run github.com/prisma/prisma-client-go"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

Task モデルを定義する。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "go run github.com/prisma/prisma-client-go"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Task {
  id String @default(cuid()) @id
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title String
  status Boolean
  desc String?
}

prisma クライアントの作成. prisma/db/ が生成される。

$ go run github.com/prisma/prisma-client-go generate

DB にスキーマを反映させる。

$ go run github.com/prisma/prisma-client-go migrate dev --name create_task

~~
~~
migrations/
  └─ 20211022082219_create_task/
    └─ migration.sql

Your database is now in sync with your schema.

既にスキーマファイルが存在する場合。

go run github.com/prisma/prisma-client-go migrate dev

prisma/migrations/20211022082219_create_task/migration.sql を見てみる。

-- CreateTable
CREATE TABLE `Task` (
    `id` VARCHAR(191) NOT NULL,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,
    `title` VARCHAR(191) NOT NULL,
    `status` BOOLEAN NOT NULL,
    `desc` VARCHAR(191) NULL,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

テーブルも作成されている。

mysql> show tables;
+--------------------+
| Tables_in_app      |
+--------------------+
| _prisma_migrations |
| Task               |
+--------------------+

Comment モデルを定義し、 Task モデルと関連付けを行う。

model Task {
  id String @default(cuid()) @id
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title String
  status Boolean
  desc String?

  comments Comment[]
}

model Comment {
  id String @default(cuid()) @id
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  content String

  task Task @relation(fields: [taskId], references: [id])
  taskId String

migration を行う。

$ go run github.com/prisma/prisma-client-go migrate dev --name add_comment_model

~~
~~
migrations/
  └─ 20211022083117_add_comment_model/
    └─ migration.sql

Your database is now in sync with your schema.

migrations/20211022083117_add_comment_model/migration.sql を見る。

-- CreateTable
CREATE TABLE `Comment` (
    `id` VARCHAR(191) NOT NULL,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,
    `content` VARCHAR(191) NOT NULL,
    `taskId` VARCHAR(191) NOT NULL,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `Comment` ADD CONSTRAINT `Comment_taskId_fkey` FOREIGN KEY (`taskId`) REFERENCES `Task`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

Prisma Studio を使う

localhost:5555 で起動する。

$ npx prisma studio

prisma api syntax

全件取得する

tasks, err := client.Task.FindMany().Exec(context.Background())

クエリで取得する

tasks, err := client.Task.FindMany(
  db.Task.Title.Equals("1st task")
  // <model>.<field>.<method>.(value) 基本的にこの形式で使う
).Exec(context.Background())

一意なデータを取得する

schema.prisma@id@unique でマークされたもののみに使用可能。

task, err := client.Task.FindUnique(
  db.Task.ID.Equals("1234567890")
).Exec(context.Background())

最初に見つかった 1 件を取得する

task, err := client.Task.FindFirst(
  db.Task.Title.Equals("1st task")
).Exec(context.Background())

スキーマのデータ型によってよしなにできる

tasks, err := client.Task.FindMany(
  // Title が "1st task" と一致する Task を取得
  db.Task.Title.Equals("1st task")

  // Title に "task" を含む Task を取得
  // db.Task.Title.Contains("task")

  // Title が "1st" から始まる Task を取得
  // db.Task.Title.StartsWith("1st")

  // Title が "task" で終わる Task を取得
  // db.Task.Title.EndsWith("task")
).Exec(context.Background())
// <Field> が 50 である <Model> を取得
db.<Model>.<Field>.Equals(50).Exec(context.Background())

// <Field> が 50 以下の <Model> を取得
db.<Model>.<Field>.Lte(50).Exec(context.Background())

// <Field> が 50 未満の <Model> を取得
db.<Model>.<Field>.Lt(50).Exec(context.Background())

// <Field> が 50 以上の <Model> を取得
db.<Model>.<Field>.Gte(50).Exec(context.Background())

// <Field> が 50 より大きいの <Model> を取得
db.<Model>.<Field>.Gte(50).Exec(context.Background())
tasks, err := client.Task.FindMany(
  // 昨日作成された task を取得する
  db.Task.CreatedAt.Equals(yesterday)

  // 過去 6 時間で作られた task を取得する (createdAt > 6 hours ago)
  // db.Task.Gt(time.Now().Add(-6 * time.Hour))

  // 過去 6 時間で作られた task を取得する (createdAt >= 6 hours ago)
  // db.Task.Gte(time.Now().Add(-6 * time.Hour))

  // 昨日作成された task を取得する
  // db.Task.Lt(time.Now().Truncate(24 * time.Hour))

  // 昨日作成された task を取得する (本日 00:00:00 を含む)
  // db.Task.Lte(time.Now().Truncate(24 * time.Hour))
).Exec(context.Background())

NULL 関連

null であるものを取得する。

db.Task.Content.EqualsOptional(nil).Exec(context.Background())

content := "string"
db.Task.Content.EqualsOptional(&content).Exec(context.Background())

他にも

db.Task.Not(
  db.Task.Title.Equals("1st task")
).Exec(context.Background())
db.Task.Or(
  db.Task.Title.Equals("1st task"),
  db.Task.Desc.Equals("new task")
).Exec(context.Background())

関連付けされたクエリ

task の title が "1st task" で comment が "new content" であるもの task を取得する。

tasks, err := client.Task.FindMany(
  db.Task.Title.Equals("1st task")
  db.Task.Comments.Some(
    db.Comment.Content.Equals("new content"),
  ),
).Exec(context.Background())

create

created, err := client.Task.CreateOne(
  db.Task.Title.Set(newTask.Title),
  db.Task.Status.Set(newTask.Status),
  db.Task.Desc.Set(newTask.Desc),
).Exec(context.Background())

関連付けのあるデータの場合。

created, err := client.Comment.CreateOne(
  db.Comment.Content.Set(newComment.Content),
  db.Comment.Task.Link(
    db.Task.ID.Equals(taskId),
  ),
).Exec(context.Background())

update

updated, err := client.Task.FindUnique(
  db.Task.ID.Equals(taskId),
).Update(
  db.Task.Title.Set(newTask.Title),
  db.Task.Status.Set(newTask.Status),
  db.Task.Desc.Set(newTask.Desc),
).Exec(context.Background())

delete

_, err := client.Task.FindUnique(
  db.Task.ID.Equals(taskId),
).Delete().Exec(context.Background())

ソースコード全体

今回は github.com/gorilla/mux を使って API サーバを作った。

package main

import (
	"backend/prisma/db"
	"context"
	"encoding/json"
	"io"
	"net/http"

	"github.com/gorilla/mux"
)

type Task struct {
	Id        string `json:"id"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	Title     string `json:"title"`
	Status    bool   `json:"status"`
	Desc      string `json:"string"`
}
type Comment struct {
	Id        string `json:"id"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	Content   string `json:"content"`
}

func main() {
	client := db.NewClient()
	if err := client.Prisma.Connect(); err != nil {
		panic(err)
	}
	defer func() {
		if err := client.Prisma.Disconnect(); err != nil {
			panic(err)
		}
	}()

	router := mux.NewRouter()

	router.HandleFunc("/tasks", func(w http.ResponseWriter, r *http.Request) {
		var newTask Task
		err := r.ParseForm()
		if err != nil {
			io.WriteString(w, "ERROR: create task")
		}
		json.NewDecoder(r.Body).Decode(&newTask)

		created, err := client.Task.CreateOne(
			db.Task.Title.Set(newTask.Title),
			db.Task.Status.Set(newTask.Status),
			db.Task.Desc.Set(newTask.Desc),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: create task")
		}

		var response struct {
			Code int    `json:"code"`
			Data []Task `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, Task{
			Id:        created.ID,
			CreatedAt: created.CreatedAt.String(),
			UpdatedAt: created.UpdatedAt.String(),
			Title:     created.Title,
			Status:    created.Status,
			Desc: func() string {
				desc, ok := created.Desc()
				if !ok {
					desc = ""
				}
				return desc
			}(),
		})
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("POST")

	router.HandleFunc("/tasks/{task_id}", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]

		task, err := client.Task.FindUnique(
			db.Task.ID.Equals(taskId),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: read task")
		}

		var response struct {
			Code int    `json:"code"`
			Data []Task `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, Task{
			Id:        task.ID,
			CreatedAt: task.CreatedAt.String(),
			UpdatedAt: task.UpdatedAt.String(),
			Title:     task.Title,
			Status:    task.Status,
			Desc: func() string {
				desc, ok := task.Desc()
				if !ok {
					desc = ""
				}
				return desc
			}(),
		})
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("GET")

	router.HandleFunc("/tasks/{task_id}", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]
		var newTask Task
		err := r.ParseForm()
		if err != nil {
			io.WriteString(w, "ERROR: update task")
		}
		json.NewDecoder(r.Body).Decode(&newTask)

		updated, err := client.Task.FindUnique(
			db.Task.ID.Equals(taskId),
		).Update(
			db.Task.Title.Set(newTask.Title),
			db.Task.Status.Set(newTask.Status),
			db.Task.Desc.Set(newTask.Desc),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: update task")
		}
		var response struct {
			Code int    `json:"code"`
			Data []Task `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, Task{
			Id:        updated.ID,
			CreatedAt: updated.CreatedAt.String(),
			UpdatedAt: updated.UpdatedAt.String(),
			Title:     updated.Title,
			Status:    updated.Status,
			Desc: func() string {
				desc, ok := updated.Desc()
				if !ok {
					desc = ""
				}
				return desc
			}(),
		})
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("POST")

	router.HandleFunc("/tasks/{task_id}", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]

		_, err := client.Task.FindUnique(
			db.Task.ID.Equals(taskId),
		).Delete().Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: delete task")
		}

		tasks, err := client.Task.FindMany().Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: read all task")
		}
		var responseData []Task
		for _, task := range tasks {
			responseData = append(responseData, Task{
				Id:        task.ID,
				CreatedAt: task.CreatedAt.String(),
				UpdatedAt: task.UpdatedAt.String(),
				Title:     task.Title,
				Status:    false,
				Desc: func() string {
					desc, ok := task.Desc()
					if !ok {
						desc = ""
					}
					return desc
				}(),
			})
		}
		var response struct {
			Code int    `json:"code"`
			Data []Task `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, responseData...)
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("DELETE")

	router.HandleFunc("/tasks", func(w http.ResponseWriter, r *http.Request) {
		tasks, err := client.Task.FindMany().Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: read all task")
		}
		var responseData []Task
		for _, task := range tasks {
			responseData = append(responseData, Task{
				Id:        task.ID,
				CreatedAt: task.CreatedAt.String(),
				UpdatedAt: task.UpdatedAt.String(),
				Title:     task.Title,
				Status:    false,
				Desc: func() string {
					desc, ok := task.Desc()
					if !ok {
						desc = ""
					}
					return desc
				}(),
			})
		}
		var response struct {
			Code int    `json:"code"`
			Data []Task `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, responseData...)
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("GET")

	router.HandleFunc("/tasks/{task_id}/comments", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]
		var newComment Comment
		err := r.ParseForm()
		if err != nil {
			io.WriteString(w, "ERROR: create comment")
		}
		json.NewDecoder(r.Body).Decode(&newComment)

		created, err := client.Comment.CreateOne(
			db.Comment.Content.Set(newComment.Content),
			db.Comment.Task.Link(
				db.Task.ID.Equals(taskId),
			),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: create comment")
		}
		var response struct {
			Code int       `json:"code"`
			Data []Comment `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, Comment{
			Id:        created.ID,
			CreatedAt: created.CreatedAt.String(),
			UpdatedAt: created.UpdatedAt.String(),
			Content:   created.Content,
		})
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("POST")

	router.HandleFunc("/tasks/{task_id}/comments", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]
		comments, err := client.Comment.FindMany(
			db.Comment.TaskID.Equals(taskId),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: read comment")
		}
		var responseData []Comment
		for _, comment := range comments {
			responseData = append(responseData, Comment{
				Id:        comment.ID,
				CreatedAt: comment.CreatedAt.String(),
				UpdatedAt: comment.UpdatedAt.String(),
				Content:   comment.Content,
			})
		}
		var response struct {
			Code int       `json:"code"`
			Data []Comment `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, responseData...)
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("GET")

	router.HandleFunc("/tasks/{task_id}/comments/{comment_id}", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		commentId := params["comment_id"]
		var newComment Comment
		err := r.ParseForm()
		if err != nil {
			io.WriteString(w, "ERROR: update comment")
		}
		json.NewDecoder(r.Body).Decode(&newComment)

		updated, err := client.Comment.FindUnique(
			db.Comment.ID.Equals(commentId),
		).Update(
			db.Comment.Content.Set(newComment.Content),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "update error")
		}
		var response struct {
			Code int       `json:"code"`
			Data []Comment `json:"comment"`
		}
		response.Code = 200
		response.Data = append(response.Data, Comment{
			Id:        updated.ID,
			CreatedAt: updated.CreatedAt.String(),
			UpdatedAt: updated.UpdatedAt.String(),
			Content:   updated.Content,
		})
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("POST")

	router.HandleFunc("/tasks/{task_id}/comments/{comment_id}", func(w http.ResponseWriter, r *http.Request) {
		params := mux.Vars(r)
		taskId := params["task_id"]
		commentId := params["comment_id"]

		_, err := client.Comment.FindUnique(
			db.Comment.ID.Equals(commentId),
		).Delete().Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: delete comment")
		}

		comments, err := client.Comment.FindMany(
			db.Comment.TaskID.Equals(taskId),
		).Exec(context.Background())
		if err != nil {
			io.WriteString(w, "ERROR: read all comment")
		}
		var responseData []Comment
		for _, comment := range comments {
			responseData = append(responseData, Comment{
				Id:        comment.ID,
				CreatedAt: comment.CreatedAt.String(),
				UpdatedAt: comment.UpdatedAt.String(),
				Content:   comment.Content,
			})
		}
		var response struct {
			Code int       `json:"code"`
			Data []Comment `json:"data"`
		}
		response.Code = 200
		response.Data = append(response.Data, responseData...)
		resp, _ := json.Marshal(response)
		w.Header().Set("Content-Type", "application/json")
		w.Write(resp)
	}).Methods("DELETE")

	server := http.Server{
		Addr:    "0.0.0.0:8080",
		Handler: router,
	}
	server.ListenAndServe()
}