Chuyển đến nội dung chính
友田 陽大
Prisma ORM
Prisma
TypeScript
PostgreSQL
データモデリング
型安全

Prisma スキーマ設計&リレーション完全ガイド:1対1・1対多・多対多、参照アクション、relationMode、複合キー、命名マッピングを型安全に設計する

Prisma(v7)のスキーマとリレーション設計を本番品質で固める実装ガイド。1対1/1対多/多対多(暗黙・明示の中間テーブル)、onDelete/onUpdateの参照アクションと既定値、relationMode(foreignKeys/prisma)、@@id/@@unique/@@indexの複合制約、@default関数、ネイティブ型、自己参照、@map/@@mapによる既存DBマッピングまで、公式ドキュメントに忠実な実コードで解説します。

Published
Reading time
12 min read
Author
友田 陽大
Chia sẻ

データベース設計は、アプリの寿命を決めます。テーブルの形・リレーションの張り方・制約の置き方を最初に間違えると、後からの修正は「マイグレーションでデータを壊さずに直す」という最高難度の作業になります。だからこそ、スキーマは最初に、正しく、意図が型に現れる形で設計する価値があります。

この記事は、Prisma ORM(v7)のスキーマとリレーション設計を本番品質で固めるための実装ガイドです。schema.prisma は単なる設定ファイルではなく、**型・クライアント・マイグレーションすべての源泉(single source of truth)**です。ここを丁寧に設計することが、その後の全コードの型安全性を決めます。Prisma 全体の本番運用はPrisma ORM 本番運用ガイド(v7)にまとめています。本記事はその中の「データモデリング」を深掘りする位置づけです。

この記事のルール:スキーマ構文・属性は Prisma 公式ドキュメント(2026年6月時点、v7系) に基づきます。題材は PostgreSQL です。プロバイダ(MySQL/SQLite/SQL Server/MongoDB/CockroachDB)により対応状況や既定のネイティブ型が異なるため、本番投入前に必ず公式ドキュメントで最新仕様を確認してください。


0. メンタルモデル:制約は「アプリの善意」ではなくDBに置く

私は型安全を徹底したプロダクトで、不正な状態をそもそも表現できないよう設計することを習慣にしています。データベースも同じです。「アプリが必ず正しい値を入れてくれる」という前提は、運用2年目には必ず破られます。一意性・NOT NULL・外部キー・参照アクションは、アプリのif文ではなくスキーマ(=DB制約)に置く——これが破綻しないデータモデリングの第一原則です。

Prisma のスキーマは、この「制約をDBに寄せる」を宣言的に書ける場所です。そして書いた制約は、そのままにも反映されます。String?string | null@uniquefindUnique の引数、リレーションは include の形——スキーマの設計判断が、コードの型として返ってきます。


1. リレーションは「2フィールドで1組」

Prisma のリレーションで最初に腹落ちさせるべきは、1つのリレーションは必ず2つのフィールドで表現されるという点です。外部キー(FK)を持つ側の @relation(fields, references) と、その逆参照側。この対応を理解すれば、すべてのリレーションは同じ原理の変奏に見えてきます。

1.1 1対多(最頻出)

最も使う形です。FKは「多」の側に置きます。

model User {
  id    Int    @id @default(autoincrement())
  posts Post[] // 逆参照(リストになる)
}

model Post {
  id       Int  @id @default(autoincrement())
  author   User @relation(fields: [authorId], references: [id]) // FK側
  authorId Int  // 実際の外部キー列
}

Post.authorPost.authorIdFKを持つ側User.posts逆参照側(配列)です。author を**省略可能(任意)**にしたいなら、両方に ? を付けます。

model Post {
  id       Int   @id @default(autoincrement())
  author   User? @relation(fields: [authorId], references: [id])
  authorId Int?  // 著者なしの投稿を許す
}

1.2 1対1

1対多との違いはたった1つ——FK列に @unique を付けることです。「1ユーザーに最大1プロフィール」を、一意制約で型・DBの両方に強制します。

model User {
  id      Int      @id @default(autoincrement())
  profile Profile? // FKを持たない側は任意(?)にする
}

model Profile {
  id     Int  @id @default(autoincrement())
  user   User @relation(fields: [userId], references: [id])
  userId Int  @unique // ← これが 1対1 の鍵
}

userId @unique がなければ、これはただの1対多です。「1対1か1対多か」は @unique の有無で決まる——この一点を覚えておけば設計で迷いません。


2. 多対多:暗黙か、明示か

多対多(記事⇄タグなど)には2つの設計があります。判断軸は「中間テーブルそのものに属性を持たせたいか」だけです。

2.1 暗黙(Prisma管理の中間テーブル)

中間テーブルに余分な列が要らないなら、Prisma に任せます。両モデルにリストを置くだけで、Prisma が中間テーブル(例 _CategoryToPost)を自動生成します。

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  categories Category[]
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]
}

暗黙の中間テーブルは、両モデルが単一の @id を持つことが条件で、fields / references / onDelete / onUpdate は指定できません。シンプルな関連には最適です。

2.2 明示(中間モデルを自分で定義)

「いつ・誰が紐付けたか」のような関連自体の属性が必要なら、中間モデルを明示します。これが実務では圧倒的に多い形です。

model Post {
  id         Int                 @id @default(autoincrement())
  title      String
  categories CategoriesOnPosts[]
}

model Category {
  id    Int                 @id @default(autoincrement())
  name  String
  posts CategoriesOnPosts[]
}

model CategoriesOnPosts {
  post       Post     @relation(fields: [postId], references: [id])
  postId     Int
  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int
  assignedAt DateTime @default(now()) // 関連の属性
  assignedBy String

  @@id([postId, categoryId]) // 複合主キー
}

@@id([postId, categoryId])複合主キーを宣言し、「同じ組み合わせは1回だけ」を保証します。assignedAt / assignedBy のような監査情報を関連に持たせられるのが明示形の強みです。

設計判断(YAGNI):迷ったら暗黙から始め、関連に属性が必要になった瞬間に明示へ移行します。先回りして全部を明示モデルにすると、ボイラープレートだけが増えます。


3. 参照アクション:既定値を信じず、意図を明示する

親レコードを削除/更新したとき、子をどうするか——onDelete / onUpdate で宣言します。ここは既定値が直感に反することがあり、事故の温床です。

model Post {
  id       Int  @id @default(autoincrement())
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId Int
}

指定できる値は Cascade / Restrict / NoAction / SetNull / SetDefault の5つ。省略時の既定は、リレーションが任意か必須かで変わります。

任意リレーション(FKが?必須リレーション
onDeleteSetNullRestrict
onUpdateCascadeCascade

要点を3つ。

  • onDelete: Cascade は「親を消したら子も消す」。コメントや明細など「親に従属する子」には妥当ですが、監査ログや決済記録のような『消えてはいけない子』に安易に付けないこと。
  • SetNull は FK が任意(?でなければ使えません。SetDefault は FK 列に @default が要ります。
  • 既定の Restrict(親に子が残っていると削除を拒否)は安全側ですが、「なんとなく既定」ではなく、毎回 onDelete を明示するのを推奨します。意図がスキーマに現れ、レビューで判断できます。

可搬性の注意Restrict は SQL Server では使えず NoAction を使います。自己参照や循環参照も SQL Server / MongoDB では NoAction が必要です。


4. relationMode:FKをDBで持つか、Prismaで持つか

relationMode は、参照整合性をDBの外部キー制約で守るか、Prisma Client のクエリロジックで疑似的に守るかの選択です。

// 既定:本物のFK制約をDBに張る(PostgreSQL等)
datasource db {
  provider     = "postgresql"
  relationMode = "foreignKeys"
}
// FK非対応/無効なDB向け(PlanetScale、シャーディング等)
datasource db {
  provider     = "mysql"
  relationMode = "prisma"
}
  • foreignKeys(既定):DBレベルで本物のFK制約と参照アクションを効かせます。整合性をDBが保証するので、通常はこれ一択です。
  • prisma:FKをサポートしない/無効化した環境(MongoDB、外部キーを張らない PlanetScale など)で、Prisma が整合性をエミュレートします。

prisma モードの最重要の落とし穴:このモードでは Migrate / db pushFK列のインデックスを自動生成しません。インデックスのないFKは結合のたびにフルスキャンを招き、遅く・高コストになります。自分で @@index を張る必要があります。

model Post {
  id     Int  @id @default(autoincrement())
  userId Int
  user   User @relation(fields: [userId], references: [id])

  @@index([userId]) // prismaモードでは必須
}

5. ID・制約・既定値

5.1 主キーと複合制約

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
}

// 複合主キー
model Member {
  orgId  Int
  userId Int
  role   String

  @@id([orgId, userId])
}

// 複合ユニーク・複合インデックス
model Post {
  id       Int    @id @default(autoincrement())
  authorId Int
  title    String

  @@unique([authorId, title]) // 同一著者の同名記事を禁止
  @@index([authorId, title])  // 検索を高速化
}

@id / @unique列レベル@@id / @@unique / @@index複数列をまとめるブロックレベルです。すべてのモデルは単一 @id か複合 @@id のいずれかで一意識別子を持つ必要があります。

5.2 既定値の生成関数

model Post {
  id        Int      @id @default(autoincrement())
  uuid      String   @default(uuid(7))   // 時系列ソート可能なUUIDv7
  cuid      String   @default(cuid(2))   // 衝突しにくい短いID
  createdAt DateTime @default(now())
  published Boolean  @default(false)
}

代表的な関数は autoincrement() / cuid()cuid(2) も可)/ uuid()uuid(4) / uuid(7))/ now() / dbgenerated(...) など。主キー戦略は設計判断です——分散環境やURLに出すIDは連番より uuid(7) / cuid(2)(推測されにくく、かつ時系列性を持てる)が安全側に倒れます。DB固有のデフォルト式が必要なときは dbgenerated("...") でDB側に委ねます。


6. スカラー型・ネイティブ型・enum・自己参照

6.1 型とネイティブ型

Prisma のスカラー型は String / Boolean / Int / BigInt / Float / Decimal / DateTime / Json / Bytes の9種。@db.*DBの物理型を上書きできます。

model Article {
  id       Int      @id @default(autoincrement())
  slug     String   @db.VarChar(200) // text ではなく varchar(200)
  body     String   @db.Text
  price    Decimal  // 金額は Float ではなく Decimal(丸め誤差を避ける)
  metadata Json     // PostgreSQL では jsonb にマップ
  tags     String[] // スカラーのリスト(PostgreSQL/CockroachDB/MongoDB)
}

設計のコツ:金額・数量など正確性が要る値に Float を使わないこと。Decimal(または整数のマイナー単位)にします。私は決済基盤で金額を整数のマイナー単位に統一し、丸め誤差を構造的に排除しました。型の選択は、そのまま正しさの設計です。

6.2 enum と自己参照

enum Role {
  USER
  ADMIN
}

model User {
  id   Int  @id @default(autoincrement())
  role Role @default(USER)

  // 自己参照(1対多):上司と部下
  managerId Int?
  manager   User?  @relation("OrgChart", fields: [managerId], references: [id])
  reports   User[] @relation("OrgChart")
}

自己参照は、両側が同じ @relation("名前") を共有することで Prisma がペアを認識します。enum は PostgreSQL/MySQL/MongoDB/CockroachDB でDBの列挙型にマップされます(SQLite/SQL Server はネイティブ非対応)。


7. 既存DBへ後付け導入する:@map と db pull

新規ではなく既存DBに Prisma を載せるケースは多く、ここで @map / @@map が効きます。コード上の名前(キャメルケース等)と物理名(スネークケース等)を分離できます。

model User {
  id        Int    @id @default(autoincrement())
  firstName String @map("first_name") // 列名は first_name
  posts     Post[]

  @@map("users") // テーブル名は users
}

既存DBからスキーマを起こすには、まずイントロスペクションで現状を取り込みます。

npx prisma db pull   # 既存DB → schema.prisma を生成(上書き)
npx prisma generate  # 型付きクライアントを生成

db pull で生成された素のスキーマに対し、@map でアプリ側の命名を整え、リレーションを @relation で明示していく——これが既存DB導入の定石です。ベースライン(既存DBを壊さずマイグレーション履歴を開始する手順)はPrisma Migrate 本番運用ガイドで詳述します。

マルチスキーマ:PostgreSQL/SQL Server では @@schema("public") でモデルを特定スキーマに割り当てられます(multiSchema 機能と datasource の schemas 指定が前提)。


8. スキーマ設計チェックリスト

本番に出す前に、私がスキーマで必ず確認する項目です。

  • すべてのモデルが @id@@id で一意識別子を持つ
  • リレーションは「FK側 @relation(fields, references) +逆参照側」が両方揃っている
  • 1対1は FK 列に @unique が付いている(付け忘れ=実質1対多)
  • 多対多は「中間テーブルに属性が要るか」で暗黙/明示を選べている
  • onDelete / onUpdate を毎回明示(特に Cascade を付けてよい子かを判断)。監査・決済の子に安易な Cascade なし
  • FK 列・検索条件列に @@indexrelationMode = "prisma" なら必須)
  • 金額・数量は Decimal か整数のマイナー単位(Float を避ける)
  • URL/外部公開IDは連番ではなく uuid(7) / cuid(2) を検討
  • 既存DBは @map/@@map でコード名と物理名を分離し、db pull で現状を取り込む
  • 一意制約は複合 @@unique まで設計し、冪等性(upsert のキー)に使えるようにする

まとめ

Prisma のスキーマ設計は、「制約をアプリではなくDBに置き、その意図を型に現れる形で宣言する」作業です。リレーションは2フィールドで1組、1対1は @unique、多対多は属性の有無で暗黙/明示、参照アクションは既定に頼らず明示、relationMode = "prisma" ではインデックスを自分で張る——この設計判断の積み重ねが、後から壊れないデータモデルを作ります。

スキーマが固まれば、次はそれをどう本番DBへ安全に適用し続けるかです。無停止のスキーマ変更とCI/CDはPrisma Migrate 本番運用ガイドへ。「データモデルの設計から、本番で壊れないDB運用までを一気通貫で固めたい」——そんな要件があれば、設計レビューから実装までお手伝いできます。

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc