🧙

Go と prisma と lit-html と ky で作るモダンな TODO アプリ

2021/03/15に公開

はじめに

以前から Go の ORM (Object Relational Mapping) 選定の為に、そこそこ時間を使っています。現状は gorp を使っていますが、満足している訳ではありません。

そんな中で見つけた prisma を試すべく、バックエンドに Goprisma を使った TODO アプリを作ってみる事にしました。

prisma とは

https://www.prisma.io/

prisma (Next-generation ORM for Node.js and TypeScript)は簡単に言うと

  • 自動生成された型付きのクライアントが付いている
  • マイグレーションが出来る
  • モデル定義から CRUD やインデックスを自動生成できる
  • PostgreSQL, MySQL, SQLite3 等をサポート
  • Prisma Studio という GUI が付いている

というモダンな ORM です。Nuxt と TypeScript 方面の産物ですが、クライアントの自動生成機能をそれぞれのプログラミング言語で実行させる事で、各々のクライアントを生成できます。Go に至っては prisma-client-go というオフィシャルのクライアントが使用できます。

構想

フロントまわりは詳しくないので、重い腰を上げたタイミングでなるべく少ない時間を使い、最新のプロダクトをキャッチアップしたいと思ったのでフロントエンドは lit-html、HTTP クライアントは ky という割と新しめの物を選ぶ事にしました。

https://lit-html.polymer-project.org/

https://github.com/sindresorhus/ky

といっても TODO アプリの部分は @ryohei さんが作られた lit-html-todo に含まれているコードをほぼほぼ使わせて頂き TypeScript から JavaScript に移植しています。TypeScript を使ってもいいのですが、モノリポになるのも微妙だし、これくらいの規模なら JavaScript のままの方が見通しが良いなと思った為です。

まずはスキーマの生成

$ npx prisma init

これを実行すると prisma に必要なファイルが生成されます。prisma/schema.prisma をテキストエディタで開き以下の様に修正しました。

修正前

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

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

generator client {
  provider = "prisma-client-js"
}

修正後

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

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

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

model Task {
  id        Int @id @default(autoincrement())
  text      String?
  completed Boolean? @default(false)
}

扱うデータベースを .env ファイルに記述します。環境変数 DATABASE_URL を設定するか .env ファイルを編集します。prisma init を実行された場合は既に生成されているので修正になります。

DATABASE_URL="postgresql://user:password@localhost/prisma-example"

DB にスキーマを反映する為に以下を実行します。

$ npx prisma db push --preview-feature

Go のクライアント生成

オフィシャルのドキュメントには prisma コマンドではなく go run github.com/prisma/prisma-client-go を使えと書いてあるのですが、モノリポにしたくないので

$ npx prisma generate

コマンドを使います。上記の通り generator client の provider に指定されているコマンドが実行されるので結局は正しく動く事になります。

REST サーバを書く

prisma は GraphQL サーバのバックエンドとして使われる事も多い様ですが、今回は REST で書く事にしました。何分、弊社はまだ GraphQL の本番投入実績が無いのでできればこの検証もあり得る形の物を作ろうと思いました。

prisma-client-go の使用感ですが、一言で言えば Facebook 社が開発している ent とほぼ一緒です。(というかそのままでは?)

e.GET("/tasks", func(c echo.Context) error {
	tasks, err := client.Task.FindMany().OrderBy(
		db.Task.ID.Order(db.ASC),
	).Exec(context.Background())
	if err != nil {
		return c.String(http.StatusBadRequest, err.Error())
	}
	return c.JSON(http.StatusOK, tasks)
})

ent もまぁ好きなのですが1点気に入らない点があり、TaskModel から client.Task.CreateOne を簡単に呼び出せない所があります。例えばタスクの更新では、テキストの更新もあれば実施済みフラグの更新もある訳です。1つずつの更新もあれば両方の更新もあり得ます。そういった場合 schema.prisma の型には ? を付与して無視可能にするのですが、そうすると prisma-client-go は値と ok を返す task.Text()text.Complete() といった関数経由でしか値を取れなくなります。つまり以下の様に1フィールドずつリクエストに値が含まれているかのチェックが必要になる訳です。

e.POST("/tasks", func(c echo.Context) error {
	var task db.TaskModel
	if err := c.Bind(&task); err != nil {
		c.Logger().Error("Bind: ", err)
		return c.String(http.StatusBadRequest, "Bind: "+err.Error())
	}
	var text *string
	if newText, ok := task.Text(); ok {
		text = &newText
	}
	var completed *bool
	if newCompleted, ok := task.Completed(); ok {
		completed = &newCompleted
	}
	newTask, err := client.Task.CreateOne(
		db.Task.Text.SetIfPresent(text),
		db.Task.Completed.SetIfPresent(completed),
	).Exec(context.Background())
	if err != nil {
		return c.String(http.StatusBadRequest, err.Error())
	}
	return c.JSON(http.StatusOK, newTask)
})

現状これらをうまく扱える方法を模索している所です。

フロントエンドのコード

前述の様に @ryohei さんの lit-html-todo のコードを使っています。ほぼほぼ同じですが JavaScript Module になっている点と done フラグが complete という名前になっているだけです。また lit-html-todo はフロントのコードしか含まれていない為、バックエンドに繋げる為に ky という HTTP クライアントを使いました。

(async () => {
  const state = store();
  const task = await ky.post('/tasks', {
    json: { text: state.inputText }
  }).json();
  store({
    tasks: [...state.tasks, task],
    inputText: ""
  })
})();

axios は使った事がありましたが今回は ky を選びました。

実行方法

go build すれば実行モジュールが生成され、実行すると http://localhost:8989 で TODO アプリが使えます。

フロントエンド側のエラー処理がほぼないので「ちゃんと書いてくれ」という方、ぜひとも pull-req をお願いいたします。

おわりに

prismalit-htmlky といった割とモダンな組み合わせで Go のアプリを書いてみました。ORM としては好き嫌いがある為、実際に業務で使えるかどうかは皆さんの目で確認してみて下さい。以下のリポジトリにソースコードを置いています。

https://github.com/mattn/go-prisma-example

良かったら遊んでみて下さい。

Discussion