Skip to content

何をした / 何が起きた

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>.tsdev または 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」)

typescript
// 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 seedtmp script (scripts/<name>.ts)
主な用途静的マスタ + dev サンプル本番 one-shot 操作・data migration
実行頻度migrate reset のたび必要なときに 1 回(または数回)
想定環境主に dev / testdev または prd 個別
冪等性必須設計次第(ガードで補う)
Prisma の関与prisma db seed で起動関与しない(tsx で直接実行)

tmp script のサンプル(本番最初の管理者投入)

typescript
// 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 のサンプル(破壊的操作はガード必須)

typescript
// 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

typescript
// 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 ファイル内で引数分岐する。

bash
npx prisma db seed -- --environment development

seed.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 初期化を両方持つときの責任分担

データseedidempotent 初期化
静的マスタ(通貨・言語)dev で投入prd で存在保証
dev サンプル(テスト商品)dev のみ投入関与しない
最初の管理者dev は弱い PW で seed に書くprd は環境変数経由で idempotent 側に書く

両方持つと「同じデータを 2 箇所で書く」状態になるが、目的が違うので重複ではなく分担と捉える。

参考