Closed9

PrismaとPlanetScale

yubrotyubrot

Prismaは次世代のORM。以下のコンポーネントを含む:

  • Prisma Client: Node.js & TypeScript向けの自動生成されるタイプセーフなクエリビルダ。
  • Prisma Migrate: 専用のマイグレーションツール
  • Prisma Studio: データベースを表示・操作するためのGUI

Prismaは専用のスキーマ言語を備える。Prisma Schemaはアプリケーションモデルの定義 (Data model) に加えて、データベースへのコネクションの定義 (Data source) と生成するPrisma Clientの定義 (Generator) を含む。

yubrotyubrot

Prisma data model

PrismaのデータモデルはModelのコレクションからなる。データモデルはPrism Schemaに直接記述してPrisma Migrateによってデータベースに反映するが、Introspectionによってデータベースからデータモデルを生成することもできるようだ。

Introspectionについては今回は追わないため、 Prisma ClientPrisma Migrate を中心としたワークフローについて学んでいく。

Accessing your database with Prisma Client

とりあえず例に従って npx prisma generate してみる。

npx prisma generate
✔ Installed the @prisma/client and prisma packages in your project
✔ Generated Prisma Client (3.11.1 | library) to ./node_modules/@prisma/client in 52ms

コード生成された結果は ./node_modules/.prisma/client に配置されていて、 @prisma/client の実装がこの位置にコード生成された結果が存在することを前提として参照している。 node_modules/ 以下そういう風にも使えるんですねえ… (参考)

これによって以下のようなクエリをタイプセーフに発行できる:

example.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
const allUsers = await prisma.user.findMany({
  include: { posts: true },
})
yubrotyubrot

Is Prisma an ORM?

To answer the question briefly: Yes, Prisma is a new kind of ORM that fundamentally differs from traditional ORMs and doesn't suffer from many of the problems commonly associated with these.

Traditional ORMs provide an object-oriented way for working with relational databases by mapping tables to model classes in your programming language. This approach leads to many problems that are caused by the object-relational impedance mismatch.

Prisma works fundamentally different compared to that. With Prisma, you define your models in the declarative Prisma schema which serves as the single source of truth for your database schema and the models in your programming language.

従来のData Mapperパターンではホストのプログラミング言語のデータモデル (に @Entity@Column でアノテーションしたもの) と関係データベースのマッピングを行うが、PrismaではPrisma Schemaファイルを中心として、自動生成されるTypeScriptのデータ型、Prisma Clientの関数を用いる。 またPrismaではマイグレーションもPrisma Schemaから導出される (他のORMでは、.NETのEntity FrameworkやDjangoのmakemigrationsも似たようなことが出来る)。

Prisma SchemaにおけるモデルはデータベースモデルやActive Recordパターンのモデルとは少し毛色が異なる。

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String? @map("post_content")
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

Prisma Schemaのモデルでは、データベースのスキーマの定義と対応するPrisma Client上での表現が共に表現されている。例えば、2つの関係フィールド author および posts としてモデル間の1:Nの関係が定義されているが、これはデータベース上のテーブルには存在しない ((Annotated) relation fields)。一方で、Foreign keyとなる authorId@relation 属性によって指定されている (Relation scalar fields)。指定されたフィールドはPrisma Client上ではread-onlyとなる。

yubrotyubrot

Prisma Schema

Prisma Schemaで一番面白いところは Is Prisma an ORM? で既に取り上げた。

最初に書いたように、Prismaは prisma generateprisma migrate (または prisma db push) でPrisma Clientのコード生成やデータソースへの反映を行うので、Prisma Schemaはこれらの定義を含む。

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

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

あとRelationsはPrisma Schemaが実際どう書かれるかというのが見えてくる。個人的な所感として、One-to-manyでもimplicitに出来そうではあるけどjoinが発生するか暗黙のフィールドが生えるかというところのトレードオフから出来ないようにしたのかなとか。


Prisma Clientについては、柔軟でType-safeなクエリビルダが得られるということで後でTypeScriptの生成結果をもう少し追ってみたいと思うが、Prisma Migrateの方をまず追ってみる。

yubrotyubrot

Prisma Migrate

Prisma MigrateはPrismaと統合されたマイグレーションツール。こちらも Is Prisma an ORM? で取り上げたが、Prismaは常に Prisma SchemaをSource of truthとする…最新のスキーマとして解釈する ので、Prisma Schemaの 変更の検知 からマイグレーションヒストリを生成する形になる。

マイグレーションヒストリは生のSQLで、このSQLがデータモデルの 変更履歴のSource of truth となる。Prisma Schemaの変更の検知からは汲み取れない変更があったときは、この生成されたSQLを直接編集することになる (例えば、フィールドのリネームはデフォルトではdrop columnとadd columnによる差分として検知される)。

具体的なワークフローについてはCLIコマンドの動作を知るとわかりやすいが、先に3つの歴史上のポイントを把握しておきたい。

  • (A) Prisma Schema:
    スキーマのSource of truthで、目指す状態
  • (B) マイグレーションヒストリを全て適用したときのスキーマ:
    Aとの差分が 新たに生成されるマイグレーションファイル(SQL)となる
  • (C) 実際のデータソースの現在のスキーマ:
    マイグレーションヒストリにデータソースに未適用のマイグレーションがある場合、CはBより古くなる

これらを考慮して、以下の一部のコマンドはshadow databaseという使い捨てのデータベースを用いる。

The shadow database is created and deleted automatically each time you run a development-focused command and is primarily used to detect problems such as schema drift.

prisma migrate dev [--create-only]

開発環境向けのマイグレーション適用&生成コマンド。以下のような動作をする:

  1. マイグレーションヒストリを最初からshadow database上でリプレイし、Cとの不整合を検知する。不整合があるときは、Prisma外でマイグレーションヒストリやデータソースのスキーマへの変更が起こっている。
  2. 新しいマイグレーションを生成する。1が終わるとshadow databaseのスキーマはBに追いつくので、Aとの差分を計算してマイグレーションヒストリにSQLとして出力できる。
  3. (--create-only が指定されていない場合) データソースに未適用のマイグレーションを適用していく。
    • 既存のマイグレーションに未適用のものがあれば、まずそれが適用されることでデータソースのスキーマはBに追いつき、
    • 次に2で生成されたマイグレーションを適用することで、データソースのスキーマはAに追いつく。
    • 変更履歴を保持する _prisma_migrations テーブルも更新する。
  4. prisma generate 相当の関連するコード生成などをトリガーする

prisma migrate deploy

こちらは本番環境向けのマイグレーション適用コマンド。このコマンドはPrisma Schemaを参照せず、マイグレーションヒストリからデータソースに未適用のマイグレーションを順に適用していく。
(適用済みのマイグレーションとマイグレーションヒストリとの間に不整合が検知された場合は事前に警告される)

prisma db push

prisma migrate 系のコマンドと異なり、 _prisma_migrations テーブル、およびマイグレーションヒストリに頼らず、データベースのスキーマとPrisma Schemaを比較して差分を推論し、その変更を実行する。 (その後 prisma migrate dev と同様に prisma generate 相当の関連するコード生成などをトリガーする)

データロスが発生する場合は --accept-data-loss オプションを必要とする。

この後追っていくPlanetScaleを使う場合は db push が推奨されている。

yubrotyubrot

PlanetScale

https://zenn.dev/tak_iwamoto/articles/b27151d22d9e6a

この記事がよくまとまっていて良かった。

Branching

ブランチ機能。コードをブランチで管理するように、PlanetScaleはデータベーススキーマをブランチで管理できる。

ブランチは最初はDevelopment branchで、promoteによってProduction branchとすることができる。Production branchは高い可用性と自動的なバックアップを伴い、スキーマの直接の変更がプロテクトされており変更にはdeploy requestを要する。

Online Schema Change

https://docs.planetscale.com/learn/how-online-schema-change-tools-work

yubrotyubrot

Prisma and PlanetScale

参考:

prisma migrate を色々と追ってきたがPlanetScaleでは prisma db push の使用が推奨される。ブランチのmerge時にはPlanetScale自身がスキーマを比較してdiffを計算するため、Prisma Migrateによるマイグレーションは実際のスキーマの変更を反映しない。

PlanetScale provides Online Schema Changes that are deployed automatically when you merge a deploy request and prevents blocking schema changes that can lead to downtime. This is different from the typical Prisma workflow which uses prisma migrate in order to generate SQL migrations for you based on changes in your Prisma schema. When using PlanetScale with Prisma, the responsibility of applying the changes is on the PlanetScale side. Therefore, there is little value to using prisma migrate with PlanetScale.

また、PlanetScaleはForeign keyをサポートしない。PrismaではReferential actionsによってデフォルトで参照整合性(Referential integrity)が保たれるが、参照整合性の維持にはデフォルトで "foreignKeys" を用いるためそのままだとPlaenetScaleでは動作しない。現在はプレビュー機能だが、Prismaの参照整合性のエミュレート機能を有効にすることができる。現在のところ参照整合性をエミュレートする設定のときはインデックスは自動で生成されないため、パフォーマンスのためインデックスの指定が必要な場合もある。

Vercel and Prisma and PlanetScale

参考:

Vercelのpreview deployment用にPlanetScaleにpreviewブランチを設けておく。ただ、この方法では並列な2つのPRが共にデータベースに変更を加えたときは競合してしまう。この辺はいずれ改善されるかもしれない。

renaming column

prisma db push だとどうするのか。Expand and contract patternによるダウンタイムを伴わない変更が提案されている。このパターンについてはPrisma Data Guideに詳しい解説がある

yubrotyubrot

PlanetScaleを使ってみる

# サインイン
$ pscale auth login

# データベースを作成
$ pscale database create fekg [--region ap-northeast]

# Development branchとしてdevブランチをmainから作成、mainをProduction branchにpromote
$ pscale branch create fekg dev
$ pscale branch promote fekg main
$ pscale branch list fekg

# データベースと接続
$ pscale connect fekg dev

# データベース接続用のクレデンシャルを生成
$ pscale password create fekg dev <name>

クレデンシャルに基づいて、接続のためのURLはPrismaの場合 mysql://<USERNAME>:<PASSWORD>@<ACCESS_HOST_URL>/<DATABASE_NAME>?sslaccept=strict となる。(PlanetScaleにVercelとのintegrationがあるがこちらは試していない)

このスクラップは2022/05/05にクローズされました