Prisma で シュッ として Atlas で カチッ とするテーブル設計
はじめに
Prisma を開発支援ツールとして使い Atlas を運用ツールとして使うのはどうだろうという思いつきのネタ記事です。
半日くらいちょろちょろと触ってみたくらいなので情報は不足しているかと思います。ちょっとしたアイデアくらいの認識でお願いします。
なぜこんなことを?
- Prisma 使ってみよ〜。お、便利じゃん。ドキュメントも読みやすい
- データはとりあえず論理削除で設計しとこ(アンチパターン?まあ一旦置いておいて。)
- 論理削除にするけど重複チェックもしたい。制約に条件追加したいな
- え、Prisma でこれどうやってやるの?ドキュメント読んても書いてなさそうだし ChatGPT に聞いてもできないって言ってる
- 自前でマイグレーションしてから取り込んでください。か。なるほど
- そうすれば反映されるのか?反映はされなかった。
- そういえば Atlas っていうツールもあったよな。
- 使ってみよ
- コマンドで定義取り込めるからユニーク制約自前でマイグレーションかけたやつ取り込んでみよ。
- お、表現できるじゃん。
- でもちょっと
hcl
ファイルの見通しが悪いな … - ん、ちょっと待てよ。 Prisma と組み合わせて使ってみては … ???
宣言的っていいですよね。
代表的なものでいうと Kubernetes のデリバリーを管理する ArgoCD があるかと思います。
( IaC も宣言的といえるのですかね。Terraform とか。)
我々開発者が完成形を定義してあげればよしなにそうなるように調整してくれます。
これは SQL におけるマイグレーションにおいても活用したいものです。
.up.sql
.down.sql
ファイルを作成して適応していくのもよいですがドキュメントを整備しておかないとこれを適用するとテーブルってどうなるんだっけ?などとなります。
マイグレーションにおいても宣言的に管理できるのはメリットがあるかと思います。
今回はそんなマイグレーションツールである Prisma と Atlas に出会い
使いやすかったし使いづらかったので組み合わせてみようと思いたったので記事にしてみました。
Prisma とは
Node.js 製のツールです。
公式ドキュメントによると次世代の Node.js および TypeScript ORM とのことです。
コード自動生成が魅力的で利用している方が多いかと思いますが今回は開発支援用のマイグレーションツールとして利用します。
Atlas とは
Go 製のデータベーススキーマをコードで管理するツールです。
HCL
JSON
SQL
形式でスキーマ定義が作成できます。
準備
uname -a
Darwin MacBook-Pro-7.local 22.6.0 Darwin Kernel Version 22.6.0: Wed Jul 5 22:22:52 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8103 arm64
docker version
Client:
Cloud integration: v1.0.35-desktop+001
Version: 24.0.5
API version: 1.43
Go version: go1.20.6
Git commit: ced0996
Built: Fri Jul 21 20:32:30 2023
OS/Arch: darwin/arm64
Context: desktop-linux
Server: Docker Desktop 4.22.1 (118664)
Engine:
Version: 24.0.5
API version: 1.43 (minimum version 1.12)
Go version: go1.20.6
Git commit: a61e2b4
Built: Fri Jul 21 20:35:38 2023
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.6.21
GitCommit: 3dce8eb055cbb6872793272b4f20ed16117344f8
runc:
Version: 1.1.7
GitCommit: v1.1.7-0-g860f061
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Prisma は Bun を利用してインストールします。
curl -fsSL https://bun.sh/install | bash
bun install prisma
bun run prisma init
bun --version
1.0.3
prisma version
prisma : 5.3.1
@prisma/client : Not found
Current platform : darwin-arm64
Query Engine (Node-API) : libquery-engine 61e140623197a131c2a6189271ffee05a7aa9a59 (at node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node)
Schema Engine : schema-engine-cli 61e140623197a131c2a6189271ffee05a7aa9a59 (at node_modules/@prisma/engines/schema-engine-darwin-arm64)
Schema Wasm : @prisma/prisma-schema-wasm 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59
Default Engines Hash : 61e140623197a131c2a6189271ffee05a7aa9a59
Studio : 0.494.0
Atras は以下のコマンドでインストールします。
curl -sSf https://atlasgo.sh | sh
atlas version
atlas version v0.14.2-75e99bd-canary
https://github.com/ariga/atlas/releases/latest
Prisma 所感
- 公式ドキュメントが見やすい
-
shema.prisma
の定義が簡単 - Prisma Studio というアプリが使える
- VSCode のプラグインが用意されている
- プラグインを使うと補完が効くのでコーディングしやすい
- index をよしなにマイグレーションしてくれるので命名をしなくてよい
Atlsa 所感
- Prisma で宣言できなかった条件付きのユニーク制約が定義できる
- Terraform を使っているのでちょっと馴染みがある
- 本番適用とかのガイドがいろいろ整っていそう
-
schema.hcl
がschema.prisma
と比べると見通しが悪い
使い方
-
schema.prisma
ファイルを編集する -
prisma db push
にてマイグレーションを実行する -
atlas schema inspect
を実行しschema.hcl
に反映させる -
schema.hcl
を調整しPrisma で定義できないものをこちらで追加する -
atlas schema apply
にてマイグレーションを実行する -
prisma db pull
にてschema.prisma
を調整する
いざ実践
今回試しに作ってみたテーブルは以下のようなものとなります。
アプリケーション側でもろもろ担保する想定で割と緩めに定義してます。
アカウントを復活させる想定で論理削除としています。
(このテーブル定義が適切かどうかの議論は一旦置いておいてください。)
schema.prisma
ファイルを用意する
0. prisma init
コマンドにて schema.prisma
ファイルを用意します。
今回はコード自動生成は利用しないので generator
は削除します。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
- generator client {
- provider = "prisma-client-js"
- }
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
また、 .env
ファイルにデータベースへの接続情報を記載します。
今回は Docker にて PostgreSQL コンテナを用意しています。
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable
schema.prisma
ファイルを編集する
1. テーブル定義を schema.prisma
に反映します。
schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model accounts {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
created_at DateTime @default(now()) @db.Timestamptz(3)
updated_at DateTime @default(now()) @db.Timestamptz(3)
deleted Boolean @default(false)
emails emails?
passwords passwords?
profiles profiles?
}
model passwords {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
value Bytes
created_at DateTime @default(now()) @db.Timestamptz(3)
updated_at DateTime @default(now()) @db.Timestamptz(3)
account_id String @unique @db.Uuid
account accounts @relation(fields: [account_id], references: [id])
@@index([account_id])
@@index([value])
}
model emails {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
value String @unique
created_at DateTime @default(now()) @db.Timestamptz(3)
updated_at DateTime @default(now()) @db.Timestamptz(3)
deleted Boolean @default(false)
account_id String @unique @db.Uuid
account accounts @relation(fields: [account_id], references: [id])
@@index([account_id])
@@index([value])
}
model profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
first_name String
family_name String
created_at DateTime @default(now()) @db.Timestamptz(3)
updated_at DateTime @default(now()) @db.Timestamptz(3)
account_id String @unique @db.Uuid
account accounts @relation(fields: [account_id], references: [id])
@@index([account_id])
}
prisma db push
にてマイグレーションを実行する
2. prisma db push
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
🚀 Your database is now in sync with your Prisma schema. Done in 119ms
postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-----------+-------+----------
public | accounts | table | postgres
public | emails | table | postgres
public | passwords | table | postgres
public | profiles | table | postgres
(4 rows)
atlas schema inspect
を実行し schema.hcl
に反映させる
3. 以下のファイルが生成されます。
schema.hcl
table "accounts" {
schema = schema.public
column "id" {
null = false
type = uuid
default = sql("gen_random_uuid()")
}
column "created_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "updated_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "deleted" {
null = false
type = boolean
default = false
}
primary_key {
columns = [column.id]
}
}
table "emails" {
schema = schema.public
column "id" {
null = false
type = uuid
default = sql("gen_random_uuid()")
}
column "value" {
null = false
type = text
}
column "created_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "updated_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "deleted" {
null = false
type = boolean
default = false
}
column "account_id" {
null = false
type = uuid
}
primary_key {
columns = [column.id]
}
foreign_key "emails_account_id_fkey" {
columns = [column.account_id]
ref_columns = [table.accounts.column.id]
on_update = CASCADE
on_delete = RESTRICT
}
index "emails_account_id_idx" {
columns = [column.account_id]
}
index "emails_account_id_key" {
unique = true
columns = [column.account_id]
}
index "emails_value_idx" {
columns = [column.value]
}
index "emails_value_key" {
unique = true
columns = [column.value]
}
}
table "passwords" {
schema = schema.public
column "id" {
null = false
type = uuid
default = sql("gen_random_uuid()")
}
column "value" {
null = false
type = bytea
}
column "created_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "updated_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "account_id" {
null = false
type = uuid
}
primary_key {
columns = [column.id]
}
foreign_key "passwords_account_id_fkey" {
columns = [column.account_id]
ref_columns = [table.accounts.column.id]
on_update = CASCADE
on_delete = RESTRICT
}
index "passwords_account_id_idx" {
columns = [column.account_id]
}
index "passwords_account_id_key" {
unique = true
columns = [column.account_id]
}
index "passwords_value_idx" {
columns = [column.value]
}
}
table "profiles" {
schema = schema.public
column "id" {
null = false
type = uuid
default = sql("gen_random_uuid()")
}
column "first_name" {
null = false
type = text
}
column "family_name" {
null = false
type = text
}
column "created_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "updated_at" {
null = false
type = timestamptz(3)
default = sql("CURRENT_TIMESTAMP")
}
column "account_id" {
null = false
type = uuid
}
primary_key {
columns = [column.id]
}
foreign_key "profiles_account_id_fkey" {
columns = [column.account_id]
ref_columns = [table.accounts.column.id]
on_update = CASCADE
on_delete = RESTRICT
}
index "profiles_account_id_idx" {
columns = [column.account_id]
}
index "profiles_account_id_key" {
unique = true
columns = [column.account_id]
}
}
schema "public" {
comment = "standard public schema"
}
schema.hcl
を調整し Prisma で定義できないものをこちらで追加する
4. このままだと論理削除したアカウントが持つメアドを再登録できないので emails に定義を追加します。
このとき index 名は prisma が生成したものを参考にしています。
index "emails_value_key" {
unique = true
columns = [column.value]
}
+ index "emails_value_not_deleted_idx" {
+ unique = true
+ columns = [column.value]
+ where = "(NOT deleted)"
+ }
}
atlas schema apply
にてマイグレーションを実行する
5. -- Planned Changes:
-- Create index "emails_value_not_is_deleted_idx" to table: "emails"
CREATE UNIQUE INDEX "emails_value_not_deleted_idx" ON "public"."emails" ("value") WHERE (NOT deleted);
✔ Apply
prisma db pull
にて schema.prisma
を調整する
6. Atlas 側のマイグレーションが Prisma 側で問題ないことを確認するためにも実行します。
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
✔ Introspected 4 models and wrote them into prisma/schema.prisma in 98ms
Run prisma generate to generate Prisma Client.
※ 2.5 ( Prisma で定義できないマイグレーションを自前SQLにて実行する)
schema.hcl
ファイルを編集するのではなく自前で SQL を作成してマイグレーションを実行して そのあと Atlas 側で取り込んで整合性をとることも可能です。
CREATE UNIQUE INDEX "emails_value_not_deleted_idx" ON "public"."emails" ("value") WHERE (NOT deleted);
結局ドキュメントは欲しい
schema.prisma
が見やすいとはいえやっぱりドキュメントは欲しいです。
個人的にお気に入りのこちらのツールを用いて自動生成します。
差分の管理
schema.prisma
は開発支援ツールとして扱いますが整合性は担保しておきたいです。
そこで CI にて prisma db pull
を実行して差分があればCIが落ちるような設定を追加しておきます。
おわりに
なんかいい感じにできそうじゃないでしょうか。
私の調査不足で Prisma が条件付き制約に対応しているのであればこちらの記事は無となります。
… そうですよね。2つのツールを意識していかないといけないです。
そして Atlas もどこまでできるのか全く深掘りできていないです。
実運用で使うかというとまだまだ調査が必要そうです。
結局ドキュメントは整備するのだし、実運用は黙って up
down
で管理ですかね。
今回作ったリポジトリはこちらに置いておきます。
各種コマンドなどを Makefile で管理しているので気になった方は覗いてみてください。
Discussion