T-CREATOR

Prisma リレーション設計早見表:1-N/N-N/自己参照/循環参照の書き方と注意点

Prisma リレーション設計早見表:1-N/N-N/自己参照/循環参照の書き方と注意点

Prisma でデータベース設計を行う際、リレーションの定義は避けて通れない重要な要素です。特に、1 対多(1-N)、多対多(N-N)、自己参照、循環参照といった複雑なリレーションパターンは、正しく理解していないとエラーに悩まされたり、パフォーマンスの問題を引き起こしたりします。

本記事では、Prisma におけるリレーション設計の全パターンを体系的に整理し、それぞれの書き方、注意点、よくあるエラーと解決策を詳しく解説します。実務でそのまま使える具体例を豊富に用意していますので、ぜひ参考にしてください。

Prisma リレーション設計早見表

各リレーションパターンの基本的な書き方と注意点を一覧にまとめました。

1-N(一対多)リレーション

#項目内容
1基本構文親: posts Post[] / 子: user User @relation(fields: [...])
2主な用途ユーザーと投稿、カテゴリーと商品など
3注意点複数リレーションがある場合は @relation("名前") で区別する
4削除制御onDelete: Cascade で親削除時に子も削除

N-N(多対多)リレーション

#項目内容
1暗黙的方式両モデルで tags Tag[] / posts Post[] と定義
2明示的方式中間モデル PostTag を作成し @@id([postId, tagId]) を定義
3使い分け基準メタデータ不要 → 暗黙 / メタデータ必要 → 明示的
4注意点暗黙的方式では中間テーブルにカラム追加不可

自己参照リレーション

#項目内容
1基本構文followers User[] @relation("UserFollows") を両方のフィールドに指定
2主な用途フォロー機能、ツリー構造、組織階層など
3注意点同じリレーション名を使用し、対応関係を明確にする
4親子関係parentId Int? で NULL 許可してルート要素を表現

循環参照リレーション

#項目内容
1基本構文各モデルが独立して外部キーとリレーションを定義
2主な用途ユーザー ↔ 投稿 ↔ コメントのような複雑な関係
3注意点クエリ時に深いネストを避けるため select で制御する
4パフォーマンス必要なデータのみ取得し、N+1 問題を回避する

よくあるエラーと解決策

#エラー原因解決策
1Ambiguous relationリレーション名の衝突@relation("名前") を明示的に指定
2Missing opposite field片側のみリレーション定義両側のモデルにリレーションフィールドを追加
3Unique constraint複合主キーの定義漏れ@@id([field1, field2]) を追加
4N+1 クエリ問題リレーションを個別に取得include または select で一度に取得

背景

データベースリレーションの重要性

現代の Web アプリケーション開発において、データベース設計は基盤となる要素です。ユーザー情報、投稿、コメント、タグなど、複数のエンティティが複雑に関連し合う構造を適切に表現する必要があります。

リレーショナルデータベースでは、これらの関係性を外部キー制約によって管理しますが、Prisma はこの関係性を TypeScript の型システムと統合し、型安全にデータアクセスを実現します。

Prisma が解決する課題

従来の ORM では、リレーション定義が煩雑で、SQL の知識が必要でした。Prisma は宣言的なスキーマ言語により、以下の利点を提供します。

  • 型安全性:リレーションを TypeScript の型として利用可能
  • 自動マイグレーション:スキーマ変更を自動的にデータベースへ反映
  • 直感的な構文@relation ディレクティブによる明確な定義

以下の図は、Prisma がどのようにスキーマ定義からデータベースと TypeScript 型を生成するかを示しています。

mermaidflowchart LR
  schema["schema.prisma<br/>(リレーション定義)"] -->|prisma generate| client["Prisma Client<br/>(TypeScript 型)"]
  schema -->|prisma migrate| db[("データベース<br/>(外部キー制約)")]
  client -->|型安全なクエリ| app["アプリケーション"]
  app -->|データ操作| db

このフローにより、スキーマを単一の真実の情報源として、データベースとアプリケーションコードの整合性を保てます。

リレーションの種類と用途

Prisma でサポートされる主なリレーションパターンは以下の通りです。

#リレーション種類説明用途例
11-N(一対多)1 つのレコードが複数のレコードと関連ユーザーと投稿
2N-N(多対多)複数のレコードが複数のレコードと関連投稿とタグ
3自己参照同じモデル内でのリレーションユーザーのフォロー関係
4循環参照複数モデル間の双方向リレーションユーザー ↔ 投稿 ↔ コメント

それぞれのパターンには、適切な定義方法と注意すべき落とし穴があります。

課題

リレーション設計でよくある問題

Prisma を使い始めた開発者が直面する典型的な課題をいくつか挙げます。

1. リレーション名の衝突エラー

同じモデル間に複数のリレーションを定義すると、Prisma はどのリレーションを参照すべきか判断できません。

typescript// ❌ エラーになる例
model User {
  id        Int    @id @default(autoincrement())
  posts     Post[]
  favorites Post[]  // Error: Ambiguous relation
}

エラーメッセージ:

sqlError: Ambiguous relation detected. The models User and Post are related multiple times. Please specify a unique relation name.

2. 外部キーの不整合

明示的に外部キーを指定しないと、Prisma が自動生成した名前と実際のデータベースカラムが一致せず、マイグレーションエラーが発生します。

3. N-N リレーションの中間テーブル設計ミス

暗黙の中間テーブルを使うべきか、明示的な中間モデルを定義すべきか判断を誤ると、後から拡張が困難になります。

4. 自己参照での無限ループ

自己参照リレーションをクエリする際、適切に includeselect を制御しないと、無限ループや深すぎるネストが発生します。

以下の図は、リレーション設計でよくあるエラーパターンを分類したものです。

mermaidflowchart TD
  start["リレーション定義"] --> check1{"複数リレーション<br/>あり?"}
  check1 -->|はい| error1["エラー: 名前衝突<br/>@relation 必要"]
  check1 -->|いいえ| check2{"外部キー指定<br/>あり?"}
  check2 -->|いいえ| error2["エラー: FK不整合<br/>@relation(fields: [...])"]
  check2 -->|はい| check3{"N-N リレーション?"}
  check3 -->|はい| error3["課題: 中間テーブル設計"]
  check3 -->|いいえ| check4{"自己参照?"}
  check4 -->|はい| error4["課題: 無限ループ対策"]
  check4 -->|いいえ| success["設計完了"]

これらの課題を理解した上で、次のセクションで具体的な解決策を見ていきましょう。

パフォーマンス上の注意点

リレーションを不適切に定義すると、以下のパフォーマンス問題が発生します。

  • N+1 クエリ問題:リレーションを個別に取得すると、クエリ数が爆発的に増加
  • 過剰な JOIN:不要なリレーションまで取得してしまう
  • インデックス不足:外部キーにインデックスが適切に設定されていない

これらの問題も、適切なリレーション設計とクエリ最適化で解決できます。

解決策

1-N(一対多)リレーションの基本

一対多リレーションは最も基本的なパターンで、「1 つの親レコードが複数の子レコードを持つ」関係を表します。

基本的な定義方法

以下は、User(ユーザー)が複数の Post(投稿)を持つ例です。

prismamodel User {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]  // 1人のユーザーが複数の投稿を持つ
}
prismamodel Post {
  id       Int    @id @default(autoincrement())
  title    String
  userId   Int    // 外部キー
  user     User   @relation(fields: [userId], references: [id])
}

ポイント

  • 親側(User)は配列型 Post[] で子を参照
  • 子側(Post)は外部キー userId@relation を定義
  • fields には自モデルのカラム、references には相手モデルのカラムを指定

複数の 1-N リレーションがある場合

同じモデル間に複数のリレーションを定義する場合は、@relation に名前を付けます。

prismamodel User {
  id              Int    @id @default(autoincrement())
  name            String
  writtenPosts    Post[] @relation("Author")
  favoritePosts   Post[] @relation("Favorites")
}
prismamodel Post {
  id          Int    @id @default(autoincrement())
  title       String
  authorId    Int
  author      User   @relation("Author", fields: [authorId], references: [id])
  favoritedBy User[] @relation("Favorites")
  favoriteIds Int[]  // 注: これは実際には中間テーブルが必要
}

注意点

  • リレーション名("Author""Favorites")を明示的に指定
  • 両側のモデルで同じ名前を使用すること

削除時の振る舞い(CASCADE)

親レコード削除時に子レコードも削除したい場合は、onDelete を指定します。

prismamodel Post {
  id       Int    @id @default(autoincrement())
  title    String
  userId   Int
  user     User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

onDelete のオプション:

#オプション説明
1Cascade親削除時に子も削除
2SetNull親削除時に外部キーを NULL に設定
3Restrict子が存在する場合は親の削除を拒否(デフォルト)
4NoActionデータベース側で処理(通常は Restrict と同じ)

2. N-N(多対多)リレーションの実装

多対多リレーションには、暗黙の中間テーブルを使う方法と、明示的な中間モデルを定義する方法があります。

暗黙の中間テーブル(シンプルなケース)

Post と Tag の関係のように、追加情報が不要な場合は暗黙の中間テーブルが便利です。

prismamodel Post {
  id    Int    @id @default(autoincrement())
  title String
  tags  Tag[]
}
prismamodel Tag {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]
}

Prisma は自動的に _PostToTag という中間テーブルを生成します。

メリット

  • シンプルで記述が簡潔
  • マイグレーションが自動

デメリット

  • 中間テーブルに追加カラムを持てない
  • 作成日時や順序などのメタデータが保存できない

明示的な中間モデル(拡張性が必要なケース)

「いつタグ付けしたか」などの情報が必要な場合は、中間モデルを明示的に定義します。

prismamodel Post {
  id       Int           @id @default(autoincrement())
  title    String
  postTags PostTag[]
}
prismamodel Tag {
  id       Int           @id @default(autoincrement())
  name     String
  postTags PostTag[]
}
prismamodel PostTag {
  postId    Int
  tagId     Int
  createdAt DateTime @default(now())

  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag       Tag      @relation(fields: [tagId], references: [id], onDelete: Cascade)

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

ポイント

  • PostTag が中間モデルとして機能
  • @@id([postId, tagId]) で複合主キーを定義
  • 追加カラム(createdAt)を自由に追加可能

以下の図は、暗黙と明示的な中間テーブルの違いを示しています。

mermaidflowchart TB
  subgraph implicit["暗黙の中間テーブル"]
    Post1["Post"] -.->|自動生成| _PostToTag["_PostToTag<br/>(postId, tagId)"]
    Tag1["Tag"] -.->|自動生成| _PostToTag
  end

  subgraph explicit["明示的な中間モデル"]
    Post2["Post"] --> PostTag["PostTag<br/>(postId, tagId, createdAt)"]
    Tag2["Tag"] --> PostTag
  end

選択基準

  • メタデータ不要 → 暗黙の中間テーブル
  • メタデータ必要 → 明示的な中間モデル

3. 自己参照リレーション

自己参照リレーションは、同じモデル内でレコード同士が関連する場合に使用します。

フォロー機能の実装例

ユーザー同士のフォロー関係を表現します。

prismamodel User {
  id         Int     @id @default(autoincrement())
  name       String
  followers  User[]  @relation("UserFollows")
  following  User[]  @relation("UserFollows")
}

注意点

  • 同じリレーション名 "UserFollows" を両方のフィールドに指定
  • followers:自分をフォローしているユーザー
  • following:自分がフォローしているユーザー

明示的な中間モデルでの実装

フォロー日時などを記録したい場合は、中間モデルを定義します。

prismamodel User {
  id         Int      @id @default(autoincrement())
  name       String
  followedBy Follow[] @relation("FollowedBy")
  following  Follow[] @relation("Following")
}
prismamodel Follow {
  followerId  Int
  followingId Int
  createdAt   DateTime @default(now())

  follower    User     @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
  following   User     @relation("FollowedBy", fields: [followingId], references: [id], onDelete: Cascade)

  @@id([followerId, followingId])
}

ポイント

  • follower(フォローする側)と following(フォローされる側)を明確に区別
  • リレーション名を逆にして対応関係を表現

ツリー構造(親子関係)の実装

カテゴリーの階層構造など、親子関係を表現する場合です。

prismamodel Category {
  id        Int        @id @default(autoincrement())
  name      String
  parentId  Int?       // NULL許可(ルートカテゴリー)
  parent    Category?  @relation("CategoryTree", fields: [parentId], references: [id], onDelete: Cascade)
  children  Category[] @relation("CategoryTree")
}

注意点

  • parentIdInt? で NULL 許可(ルート要素のため)
  • 親が NULL の場合はルートカテゴリーを表す

4. 循環参照への対応

複数のモデルが相互に参照し合う場合、適切に設計しないとエラーが発生します。

基本的な循環参照

User → Post → Comment の関係で、各モデルが相互に参照する例です。

prismamodel User {
  id       Int       @id @default(autoincrement())
  name     String
  posts    Post[]
  comments Comment[]
}
prismamodel Post {
  id       Int       @id @default(autoincrement())
  title    String
  userId   Int
  user     User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  comments Comment[]
}
prismamodel Comment {
  id      Int    @id @default(autoincrement())
  content String
  userId  Int
  postId  Int
  user    User   @relation(fields: [userId], references: [id], onDelete: Cascade)
  post    Post   @relation(fields: [postId], references: [id], onDelete: Cascade)
}

ポイント

  • 各モデルが独立して外部キーを持つ
  • 循環参照そのものは問題ではないが、クエリ時に注意が必要

クエリ時の注意点

循環参照がある場合、include を使うと深いネストが発生します。

typescript// ❌ 深すぎるネストになる可能性
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        comments: {
          include: {
            user: true, // 元の User に戻ってしまう
          },
        },
      },
    },
  },
});

解決策:必要なデータのみを選択的に取得します。

typescript// ✅ 必要な深さまでに制限
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: {
        comments: {
          select: {
            id: true,
            content: true,
            // user は含めない
          },
        },
      },
    },
  },
});

具体例

実践例 1:ブログシステムのリレーション設計

実際のブログシステムを想定し、複数のリレーションパターンを組み合わせた設計例を示します。

要件

  • ユーザーが投稿を作成(1-N)
  • 投稿に複数のタグを付与(N-N)
  • ユーザー同士がフォロー(自己参照)
  • 投稿にコメント(1-N、循環参照)

スキーマ全体像

prisma// ユーザーモデル
model User {
  id         Int       @id @default(autoincrement())
  email      String    @unique
  name       String
  createdAt  DateTime  @default(now())

  // 1-N: ユーザーと投稿
  posts      Post[]

  // 1-N: ユーザーとコメント
  comments   Comment[]

  // 自己参照: フォロー関係
  followedBy Follow[]  @relation("FollowedBy")
  following  Follow[]  @relation("Following")
}
prisma// 投稿モデル
model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String
  published Boolean   @default(false)
  createdAt DateTime  @default(now())

  // 1-N: ユーザーと投稿(多側)
  authorId  Int
  author    User      @relation(fields: [authorId], references: [id], onDelete: Cascade)

  // N-N: 投稿とタグ
  postTags  PostTag[]

  // 1-N: 投稿とコメント
  comments  Comment[]
}
prisma// タグモデル
model Tag {
  id       Int       @id @default(autoincrement())
  name     String    @unique
  postTags PostTag[]
}
prisma// 中間テーブル: 投稿とタグ
model PostTag {
  postId    Int
  tagId     Int
  createdAt DateTime @default(now())

  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag       Tag      @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([postId, tagId])
  @@index([postId])
  @@index([tagId])
}
prisma// コメントモデル
model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())

  // 循環参照: ユーザー
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)

  // 循環参照: 投稿
  postId    Int
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
}
prisma// フォロー関係モデル
model Follow {
  followerId  Int
  followingId Int
  createdAt   DateTime @default(now())

  follower    User     @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
  following   User     @relation("FollowedBy", fields: [followingId], references: [id], onDelete: Cascade)

  @@id([followerId, followingId])
  @@index([followerId])
  @@index([followingId])
}

スキーマのリレーション構造図

以下の図は、各モデルの関係性を視覚的に表現したものです。

mermaiderDiagram
  User ||--o{ Post : "作成"
  User ||--o{ Comment : "投稿"
  Post ||--o{ Comment : "含む"
  Post ||--o{ PostTag : "持つ"
  Tag ||--o{ PostTag : "持つ"
  User ||--o{ Follow : "フォローする"
  User ||--o{ Follow : "フォローされる"

  User {
    int id PK
    string email
    string name
  }

  Post {
    int id PK
    string title
    int authorId FK
  }

  Tag {
    int id PK
    string name
  }

  PostTag {
    int postId FK
    int tagId FK
    datetime createdAt
  }

  Comment {
    int id PK
    string content
    int authorId FK
    int postId FK
  }

  Follow {
    int followerId FK
    int followingId FK
    datetime createdAt
  }

図で理解できる要点

  • User は Post、Comment、Follow の 3 つのリレーションを持つ
  • PostTag が Post と Tag の中間テーブルとして機能
  • Follow モデルで User の自己参照を実現

実践例 2:クエリの実装例

定義したスキーマを使って、実際のデータ操作を行う例を示します。

投稿を作成してタグを付与

typescript// 投稿作成とタグの関連付け
const createPostWithTags = async () => {
  const post = await prisma.post.create({
    data: {
      title: 'Prisma リレーション完全ガイド',
      content: 'リレーションの使い方を解説します',
      published: true,
      author: {
        connect: { id: 1 }, // 既存ユーザーと関連付け
      },
      postTags: {
        create: [
          { tag: { connect: { id: 1 } } }, // 既存タグ
          { tag: { create: { name: 'Prisma' } } }, // 新規タグ
        ],
      },
    },
    include: {
      author: true,
      postTags: {
        include: {
          tag: true,
        },
      },
    },
  });

  return post;
};

ポイント

  • connect で既存レコードと関連付け
  • create で新規レコードを同時作成
  • include で関連データも一緒に取得

フォロー関係を作成

typescript// ユーザーをフォロー
const followUser = async (
  followerId: number,
  followingId: number
) => {
  const follow = await prisma.follow.create({
    data: {
      followerId,
      followingId,
    },
    include: {
      follower: {
        select: {
          id: true,
          name: true,
        },
      },
      following: {
        select: {
          id: true,
          name: true,
        },
      },
    },
  });

  return follow;
};

フォロワーの投稿を取得(複雑なリレーション)

typescript// 自分がフォローしているユーザーの投稿を取得
const getFollowingPosts = async (userId: number) => {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    include: {
      following: {
        include: {
          following: {
            include: {
              posts: {
                where: {
                  published: true,
                },
                orderBy: {
                  createdAt: 'desc',
                },
                take: 10,
              },
            },
          },
        },
      },
    },
  });

  return (
    user?.following.flatMap((f) => f.following.posts) || []
  );
};

ポイント

  • ネストした include で複数階層のリレーションを辿る
  • where でフィルタリング
  • orderBytake でページネーション

コメント付き投稿の取得

typescript// 投稿とコメント、各作成者情報を取得
const getPostWithComments = async (postId: number) => {
  const post = await prisma.post.findUnique({
    where: { id: postId },
    include: {
      author: {
        select: {
          id: true,
          name: true,
        },
      },
      comments: {
        include: {
          author: {
            select: {
              id: true,
              name: true,
            },
          },
        },
        orderBy: {
          createdAt: 'asc',
        },
      },
      postTags: {
        include: {
          tag: true,
        },
      },
    },
  });

  return post;
};

実践例 3:よくあるエラーと解決方法

実際の開発で遭遇しやすいエラーと、その対処法を紹介します。

エラー 1:リレーション名の衝突

javascriptError: Ambiguous relation detected. The fields `posts` and `favorites` on model `User` both refer to `Post`. Please provide different relation names.

原因: 同じモデル間に複数のリレーションがあるが、リレーション名を指定していない。

解決方法

prisma// ❌ エラーが発生
model User {
  id        Int    @id
  posts     Post[]
  favorites Post[]
}
prisma// ✅ リレーション名を指定
model User {
  id        Int    @id
  posts     Post[] @relation("AuthoredPosts")
  favorites Post[] @relation("FavoritePosts")
}

model Post {
  id         Int   @id
  authorId   Int
  author     User  @relation("AuthoredPosts", fields: [authorId], references: [id])
  favoritedBy User[] @relation("FavoritePosts")
}

エラー 2:外部キーの不整合

javascriptError: The relation field `author` on Model `Post` is missing an opposite relation field on the model `User`. Either run `prisma format` or add it manually.

原因: 片側のモデルにしかリレーションを定義していない。

解決方法

prisma// ❌ User 側にリレーションがない
model User {
  id   Int    @id
  name String
}

model Post {
  id       Int  @id
  authorId Int
  author   User @relation(fields: [authorId], references: [id])
}
prisma// ✅ 両側にリレーションを定義
model User {
  id    Int    @id
  name  String
  posts Post[]  // これが必要
}

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

エラー 3:複合主キーの定義ミス

javascriptError: Error validating: A unique constraint covering the columns `[postId,tagId]` on the table `PostTag` is missing.

原因: 中間テーブルに複合主キーを定義していない。

解決方法

prisma// ❌ 主キーがない
model PostTag {
  postId Int
  tagId  Int
  post   Post @relation(fields: [postId], references: [id])
  tag    Tag  @relation(fields: [tagId], references: [id])
}
prisma// ✅ 複合主キーを定義
model PostTag {
  postId Int
  tagId  Int
  post   Post @relation(fields: [postId], references: [id])
  tag    Tag  @relation(fields: [tagId], references: [id])

  @@id([postId, tagId])  // これが必要
}

エラー 4:N+1 クエリ問題

N+1 問題は、リレーションを個別に取得することで大量のクエリが発行される問題です。

typescript// ❌ N+1 問題が発生(投稿数 × クエリ)
const posts = await prisma.post.findMany();
for (const post of posts) {
  const author = await prisma.user.findUnique({
    where: { id: post.authorId },
  });
  console.log(`${post.title} by ${author?.name}`);
}

解決方法include または select で一度に取得します。

typescript// ✅ 1 回のクエリで取得
const posts = await prisma.post.findMany({
  include: {
    author: true,
  },
});

posts.forEach((post) => {
  console.log(`${post.title} by ${post.author.name}`);
});

実践例 4:パフォーマンス最適化

リレーションクエリのパフォーマンスを向上させるテクニックを紹介します。

インデックスの追加

外部キーには自動的にインデックスが作成されますが、複合条件での検索が多い場合は追加インデックスが有効です。

prismamodel Post {
  id        Int      @id @default(autoincrement())
  title     String
  published Boolean
  authorId  Int
  createdAt DateTime @default(now())

  author    User     @relation(fields: [authorId], references: [id])

  @@index([authorId, published])  // 複合インデックス
  @@index([createdAt(sort: Desc)])  // 降順インデックス
}

効果

  • where: { authorId: 1, published: true } のクエリが高速化
  • 新着順の取得が効率的に

必要なフィールドのみ取得

select を使って、不要なデータの取得を避けます。

typescript// ❌ すべてのフィールドを取得
const posts = await prisma.post.findMany({
  include: {
    author: true, // author のすべてのフィールド
  },
});
typescript// ✅ 必要なフィールドのみ
const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    author: {
      select: {
        name: true, // name のみ
      },
    },
  },
});

集計クエリの活用

リレーション先のカウントだけ必要な場合は、_count を使用します。

typescript// コメント数だけ取得
const posts = await prisma.post.findMany({
  include: {
    _count: {
      select: {
        comments: true,
      },
    },
  },
});

// 結果: posts[0]._count.comments = 5

まとめ

本記事では、Prisma におけるリレーション設計の全パターンを体系的に解説しました。

重要なポイントのおさらい

  1. 1-N リレーション:最も基本的なパターン。親側は配列型、子側は外部キーと @relation を定義します。
  2. N-N リレーション:シンプルな場合は暗黙の中間テーブル、メタデータが必要な場合は明示的な中間モデルを使い分けます。
  3. 自己参照:同じモデル内のリレーションには、必ずリレーション名を指定してください。
  4. 循環参照:複数モデル間の相互参照は問題ありませんが、クエリ時の深さに注意が必要です。

実装時のチェックリスト

#確認項目詳細
1リレーション名同じモデル間に複数のリレーションがある場合は必須
2外部キー定義fieldsreferences を正しく指定
3削除時の振る舞いonDelete: Cascade などを適切に設定
4インデックス検索条件に応じて @@index を追加
5複合主キー中間テーブルには @@id を定義

エラー対策のベストプラクティス

  • スキーマ変更後は必ず yarn prisma format で検証する
  • マイグレーション前に yarn prisma validate でエラーチェックする
  • リレーションが複雑になったら、ER 図を描いて整理する
  • クエリのパフォーマンスは、開発環境でも SQL ログを確認する

Prisma のリレーション設計は、最初は複雑に感じるかもしれません。しかし、基本パターンを理解し、適切な構文を使い分けることで、型安全で保守性の高いデータベース設計が実現できます。

本記事で紹介した設計パターンを参考に、ぜひ実際のプロジェクトで活用してみてください。リレーションを正しく設計することで、データの整合性が保たれ、開発効率が大きく向上するでしょう。

関連リンク