何をした / 何が起きた
Prisma 6 を採用しているプロジェクトで、本番初期管理者の投入・データクリーンアップ・dev 撮影前リセットなどの補助スクリプトをどこに置くべきか判断するために、Prisma 公式の seed / migration の推奨運用を一次情報で調べた。判断軸を 4 レイヤーに整理した。
なぜそうした(背景・判断)
prisma/seed.ts に「最初の管理者作成」「prd 空リセット」を混ぜようとしたとき、それが公式の意図に沿うか判断できなかった。Prisma maintainer は production の初期データを seed ではなくアプリ側に書くよう推奨している(Discussion #15890)。一方、現実のプロジェクトは tmp スクリプトを prisma/ 配下に置く慣習が定着しており、公式の理想と現実の妥協のスペクトラムでどこに着地させるかの判断が要る。
再利用できる形
Prisma を扱う 4 レイヤーの全体マップ
─────────────────────────────────────────────────────────────
[1] migration ─ スキーマ変更(順序保証あり・自動実行・必須)
[2] seed ─ アプリ起動の前提となる静的マスタ + dev/test サンプル
[3] tmp script ─ 一度限り or 必要時の手動運用(管理者投入・データ修正)
[4] idempotent ─ アプリ起動時に「存在チェック → 不在なら作成」を回す初期化
─────────────────────────────────────────────────────────────prisma db seed は [2] だけを担う。[3] と [4] は seed の枠の外で扱うのが Prisma 公式の立場。
レイヤーごとの責任分担
| レイヤー | 担当する内容 | 実行タイミング | 環境 | 冪等性 |
|---|---|---|---|---|
| migration | スキーマ変更 | migrate dev (ローカル) / migrate deploy (CI) | dev / staging / prd | 順序保証で実現 |
| seed | 言語・通貨マスタ、dev 用テストデータ | migrate reset の後、または明示的に db seed | 主に dev / test | 何度流しても同じ結果 |
| tmp script | 本番管理者投入、data migration、dev リセット | 運用者が手動で tsx scripts/<name>.ts | dev または prd 個別 | 設計次第(ガード必須) |
| idempotent | 言語マスタの存在保証、最初の管理者の自動投入 | アプリ起動時に毎回 | 全環境 | 必ず冪等に書く |
migration: dev と prd の使い分け
公式は migrate dev の prd 利用を「never」と明言している。
[ローカル dev] [CI / prd デプロイ]
prisma migrate dev ──► prisma migrate deploy
│ │
├─ shadow DB を毎回作って ├─ 既存 migration を順に適用
│ ドリフト検出(対話あり) ├─ shadow DB 不要
├─ ドリフト時 DB リセット提案 ├─ advisory lock で冪等
└─ 開発専用 └─ 非対話・production 用Development and production | Prisma Docs
> "migrate dev is a development command and should NEVER be used in a
> production environment."理由は単純で、migrate dev はドリフト検出時に DB 全リセットを提案する対話処理を含むため。production で誤実行すると全データ消失リスクがある。CI 側で 1 点だけ覚えておきたいのは、prisma パッケージは dependencies に置く(devDependencies だと Vercel など prod ビルド時に prune されて migrate deploy できなくなる)。
seed: 公式が定義する 2 つの用途
Seeding | Prisma Docs
> "Populate your database with data that is required for your application
> to start, such as a default language or currency."公式が seed のユースケースとして挙げているのは次の 2 つだけ。
- アプリが起動するために必須な静的データ(言語・通貨マスタなど)
- dev / test 環境のサンプルデータ(
migrate resetのたびに再投入される)
それ以外、特に「最初の管理者作成」や「本番データのクリーンアップ」は seed の責任ではないというのが公式の立場。
seed.ts のサンプル(架空の EC ショップ「mini-mart」)
// prisma/seed.ts
// dev / test 環境専用。`migrate reset` で毎回流れる。
// 弱いパスワード・ダミーリードを含むため prd には絶対に流さない。
import { PrismaClient } from '@prisma/client'
import { hash } from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
// 1. アプリ起動に必要な静的マスタ(通貨)
await prisma.currency.createMany({
data: [
{ code: 'JPY', symbol: '¥', name: '日本円' },
{ code: 'USD', symbol: '$', name: '米ドル' },
],
skipDuplicates: true,
})
// 2. dev 用のサンプルユーザー(テスト用の弱い PW で OK)
const passwordHash = await hash('password', 10)
await prisma.user.create({
data: {
email: 'admin@example.com',
passwordHash,
role: 'ADMIN',
},
})
// 3. dev 用のサンプル商品 5 件
await prisma.product.createMany({
data: [
{ name: 'コーヒー豆 200g', priceJpy: 1200 },
{ name: 'マグカップ', priceJpy: 1800 },
// ...
],
})
}
main().finally(() => prisma.$disconnect())tmp script: 公式が直接定義しない「現実の妥協」枠
Prisma 公式は seed と tmp script を明示的に区別していない。ただし maintainer の発言と実プロジェクトの慣習で、次の分担が定着している。
| 観点 | prisma db seed | tmp script (scripts/<name>.ts) |
|---|---|---|
| 主な用途 | 静的マスタ + dev サンプル | 本番 one-shot 操作・data migration |
| 実行頻度 | migrate reset のたび | 必要なときに 1 回(または数回) |
| 想定環境 | 主に dev / test | dev または prd 個別 |
| 冪等性 | 必須 | 設計次第(ガードで補う) |
| Prisma の関与 | prisma db seed で起動 | 関与しない(tsx で直接実行) |
tmp script のサンプル(本番最初の管理者投入)
// scripts/create-admin.ts
// 本番最初の管理者を投入する。一度実行したら基本それきり。
// ADMIN_EMAIL / ADMIN_PASSWORD を環境変数で渡し、平文をリポに残さない。
//
// ADMIN_EMAIL=... ADMIN_PASSWORD=... tsx scripts/create-admin.ts
//
// 同メールがあればパスワードを更新する upsert にしておくと、流し直しても安全。
import { PrismaClient } from '@prisma/client'
import { hash } from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
const email = process.env.ADMIN_EMAIL
const password = process.env.ADMIN_PASSWORD
if (!email || !password) {
throw new Error('ADMIN_EMAIL と ADMIN_PASSWORD を環境変数で指定してください')
}
const passwordHash = await hash(password, 10)
await prisma.user.upsert({
where: { email },
update: { passwordHash, role: 'ADMIN' },
create: { email, passwordHash, role: 'ADMIN' },
})
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(() => prisma.$disconnect())tmp script のサンプル(破壊的操作はガード必須)
// scripts/reset-prd-empty.ts
// 本番立ち上げ前に業務データを空にする。マスタと管理者は残す。
// 誤実行防止のため CONFIRM=YES を必須にする。
//
// CONFIRM=YES DATABASE_URL=... tsx scripts/reset-prd-empty.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
if (process.env.CONFIRM !== 'YES') {
throw new Error('CONFIRM=YES を付けて実行してください(破壊的操作のガード)')
}
await prisma.order.deleteMany({})
await prisma.product.deleteMany({})
// currencies / users は温存
}
main().catch((e) => { console.error(e); process.exit(1) }).finally(() => prisma.$disconnect())idempotent 初期化: Prisma 公式が「production 必須データ」に推奨する形
最初の管理者やマスタの存在保証を、アプリ起動時に毎回回す形に組み込む方式。Prisma maintainer が production 用途で推奨している唯一の道(Discussion #15890)。
"incorporate this seed into the application code" with logic that "check[s] for existence of this data"
idempotent 初期化のサンプル(NestJS の OnApplicationBootstrap)
// src/bootstrap/initial-data.service.ts
// アプリ起動時に必須データの存在を確認し、不在なら作る。
// 何度起動しても同じ状態に収束する(冪等)。
import { Injectable, OnApplicationBootstrap, Logger } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { hash } from 'bcryptjs'
@Injectable()
export class InitialDataService implements OnApplicationBootstrap {
private readonly logger = new Logger(InitialDataService.name)
constructor(private readonly prisma: PrismaService) {}
async onApplicationBootstrap(): Promise<void> {
await this.ensureCurrencies()
await this.ensureInitialAdmin()
}
private async ensureCurrencies(): Promise<void> {
const count = await this.prisma.currency.count()
if (count > 0) return
await this.prisma.currency.createMany({
data: [
{ code: 'JPY', symbol: '¥', name: '日本円' },
{ code: 'USD', symbol: '$', name: '米ドル' },
],
})
this.logger.log('通貨マスタを投入しました')
}
private async ensureInitialAdmin(): Promise<void> {
const email = process.env.INITIAL_ADMIN_EMAIL
const password = process.env.INITIAL_ADMIN_PASSWORD
if (!email || !password) return
const existing = await this.prisma.user.findUnique({ where: { email } })
if (existing) return
await this.prisma.user.create({
data: { email, passwordHash: await hash(password, 10), role: 'ADMIN' },
})
this.logger.log(`初期管理者 ${email} を投入しました`)
}
}利点は次のとおり。
- 環境変数を消せば次回起動時に作成は走らない(運用での「鍵を抜く」感覚で扱える)
- スクリプト実行を運用者に依頼する手間が消える
- 本番でも安全に何度でも起動できる
ただしロジックがアプリコードに混ざるトレードオフがあり、startup 時間が伸びる。マスタ数が多いケースでは別道(migration の INSERT を組み合わせる)を検討する。
環境別 seed: 公式は分割を推奨しない
seed.dev.ts / seed.prd.ts のような複数ファイル分割を公式は明示的に推奨していない。代わりに 1 ファイル内で引数分岐する。
npx prisma db seed -- --environment developmentseed.ts 内で process.argv を解析して分岐する。production で prisma db seed を流すかは公式から明確な禁止はないが、maintainer は dev フェーズ向けと明言している。
落とし穴・前提
migrate dev を prd で実行するな(最重要)
公式が「never」レベルで禁じている。migrate dev はドリフト検出時に DB リセットを提案する対話処理を含むため、production で誤実行すると全データ消失リスクがある。prd には migrate deploy だけを使う。
shadow database は dev 限定
prisma migrate dev 実行時のみ作られる一時的な第 2 DB。既存 migration を全件再実行して dev DB と比較しドリフトを検出するために使う。prd 用コマンド(migrate deploy, migrate resolve)からは参照されない。CI で migration の整合性を確認するときも shadow DB は要らない。
Prisma 7 で seed 周辺の挙動が変わる(先取り)
v7 で次が変わる。今から v6 で設計するときも v7 を念頭に置きたい。
migrate dev/migrate reset実行時の自動 seed が廃止。明示的なprisma db seedのみ--skip-seedフラグが削除- 設定が
schema.prismaからprisma.config.tsに移管(seed コマンド定義もこちら)
「seed は明示的に呼ぶもの」という位置づけが v7 で強くなる。「migrate reset → 自動 seed」に依存した dev フローを組んでいる場合は v7 で破綻する。
prisma パッケージは devDependencies ではなく dependencies に
Vercel など本番ビルド時に devDeps を prune する環境では、prisma を devDeps に入れると migrate deploy を実行できなくなる。@prisma/client だけでなく prisma 自体を dependencies に置く。
tmp script に冪等性とガードを必ず添える
prd-reset のような破壊的スクリプトは CONFIRM=YES のような明示ガードを必ず付ける。create-admin は upsert にしておくと再実行時の事故が減る。Prisma が冪等性を保証してくれるわけではなく、書き手が設計する責任。
seed と idempotent 初期化を両方持つときの責任分担
| データ | seed | idempotent 初期化 |
|---|---|---|
| 静的マスタ(通貨・言語) | dev で投入 | prd で存在保証 |
| dev サンプル(テスト商品) | dev のみ投入 | 関与しない |
| 最初の管理者 | dev は弱い PW で seed に書く | prd は環境変数経由で idempotent 側に書く |
両方持つと「同じデータを 2 箇所で書く」状態になるが、目的が違うので重複ではなく分担と捉える。
参考
- Seeding | Prisma Documentation
- Development and production | Prisma Documentation
- About the shadow database | Prisma Documentation
- Deploying database changes with Prisma Migrate | Prisma Documentation
- Seed database in production · prisma/prisma Discussion #15890
- Upgrade to Prisma ORM 7 | Prisma Documentation
- Prisma 6: Better Performance, More Flexibility & Type-Safe SQL | Prisma Blog