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

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

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Prisma, TypeScript, PostgreSQL, データモデリング, 型安全
- URL: https://tomodahinata.com/blog/prisma-schema-data-modeling-relations-design-guide

## 要点

- スキーマは唯一の真実源。リレーションは常に2フィールドで1組（@relationの外部キー側＋逆参照側）。1対1はFK側を@uniqueにして表現する
- 多対多は『中間テーブルに属性が要るか』で選ぶ。要らなければ暗黙（Prisma管理）、要れば明示の中間モデル＋@@id([a,b])
- 参照アクションの既定は罠。任意リレーションのonDeleteはSetNull、必須リレーションはRestrict。意図を必ず明示する
- relationMode=prismaはFK非対応DB（PlanetScale等）向け。この時インデックスは自動生成されないので@@indexを自分で張る
- 既存DBには@map/@@mapでコード名と物理名を分離して後付け導入。db pullで現状を取り込み、命名は型側で整える

---

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

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

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

---

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

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

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

---

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

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

### 1.1 1対多（最頻出）

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

```prisma
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.author` と `Post.authorId` が**FKを持つ側**、`User.posts` が**逆参照側**（配列）です。`author` を**省略可能（任意）**にしたいなら、両方に `?` を付けます。

```prisma
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の両方に強制します。

```prisma
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`）を自動生成します。

```prisma
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 明示（中間モデルを自分で定義）

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

```prisma
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` で宣言します。**ここは既定値が直感に反することがあり、事故の温床**です。

```prisma
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が`?`） | 必須リレーション |
| --- | --- | --- |
| `onDelete` | `SetNull` | `Restrict` |
| `onUpdate` | `Cascade` | `Cascade` |

要点を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 のクエリロジックで疑似的に守る**かの選択です。

```prisma
// 既定：本物のFK制約をDBに張る（PostgreSQL等）
datasource db {
  provider     = "postgresql"
  relationMode = "foreignKeys"
}
```

```prisma
// FK非対応/無効なDB向け（PlanetScale、シャーディング等）
datasource db {
  provider     = "mysql"
  relationMode = "prisma"
}
```

- **`foreignKeys`（既定）**：DBレベルで本物のFK制約と参照アクションを効かせます。整合性をDBが保証するので、通常はこれ一択です。
- **`prisma`**：FKをサポートしない/無効化した環境（MongoDB、外部キーを張らない PlanetScale など）で、Prisma が整合性をエミュレートします。

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

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

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

---

## 5. ID・制約・既定値

### 5.1 主キーと複合制約

```prisma
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 既定値の生成関数

```prisma
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の物理型**を上書きできます。

```prisma
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 と自己参照

```prisma
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` が効きます。コード上の名前（キャメルケース等）と物理名（スネークケース等）を**分離**できます。

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

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

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

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

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

> **マルチスキーマ**：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 列・検索条件列に `@@index`（`relationMode = "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 本番運用ガイド](/blog/prisma-migrate-production-zero-downtime-cicd-guide)へ。「**データモデルの設計から、本番で壊れないDB運用までを一気通貫で固めたい**」——そんな要件があれば、設計レビューから実装までお手伝いできます。
