はじめに
ここまでの記事では、NestJSとPrismaを使って、記事投稿APIの基本的なCRUD処理を実装し、ログ出力やエラーハンドリング、テスト環境の整備に取り組んできました。
第4回となる今回は、Prismaを使ったデータベース設計にもう一歩踏み込み、
複数のモデル定義やリレーション設計、マイグレーション管理といった、より実践的な運用を視野に入れた内容を整理していきます。
また、PrismaClientのインスタンス設計(PrismaServiceとPrismaModuleによるDI対応)についてはすでに導入済みのため、今回はその振り返りを踏まえつつ、さらに複数モデル間の関係性や、開発〜本番を意識したデータベース運用まで広げていく流れとします。
本記事の対象読者は、以下のような方を想定しています。
- NestJSとPrismaを使ったバックエンド開発をこれから本格的に進めたい方
- データベース設計にまだ不安があり、リレーション定義やマイグレーション管理のベストプラクティスを押さえておきたい方
- Prismaを導入したものの、複数モデル間の連携や運用方針に悩んでいる方
なお、この記事は第3回までの記事を踏まえた続きの内容となっています。
もし未読の場合は、まず第1回から第3回までを順に読んでいただくことをおすすめします。
第1回
第2回
第3回
この連載の全体像と今回の位置づけ
連載構成
- NestJSで始めるWebアプリ開発 ─ ブログサイトを作りながら学ぶプロジェクト構成と設定管理
- NestJSで記事投稿APIを作ろう ─ Prisma導入とCRUD実装の基本
- アプリの信頼性を高める ─ ロギング・エラーハンドリング・テスト戦略
- PrismaとDB設計を深掘る ─ モデル・リレーション・運用設計 ←
今回の記事
- ReactでUIを構築して本番へデプロイ ─ Dockerと環境構築
Prismaで複数モデルを定義する
この章では、実際に複数のモデルをPrismaスキーマファイルに定義する方法を整理します。
アプリケーションが少しずつ大きくなっていく中で、単一のモデルだけでデータを管理し続けることは現実的ではありません。
User、Post、Commentといったように、複数のモデルが存在し、それらの関係性を適切に設計することが必要になります。
今回は、シンプルな例として User
モデルと Post
モデルを定義してみます。
UserモデルとPostモデルの例
Prismaのスキーマファイルに、次のように定義します。
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
このように、User
と Post
の間に1対多(1:N)のリレーションを定義しています。
1人のユーザーが複数の投稿を持ち、各投稿は必ず1人のユーザーに紐づく構成です。
型補完を活かしたモデル定義のすすめ
Prismaの良さは、スキーマファイルを書いた時点で、
TypeScript側にも型が即座に生成されることにあります(Prisma Client)。
モデルを定義すると、次のような型安全な補完が利用できるようになります。
const newPost = await prisma.post.create({
data: {
title: 'My first post',
content: 'Hello World!',
author: {
connect: { id: 1 },
},
},
});
このとき、フィールド名や型ミスはコンパイル時点で検出されるため、ランタイムエラーのリスクが大きく減ります。
モデル設計の段階で、フィールド名や型の意図を丁寧に整理しておくことが、後の開発体験を大きく左右します。
Prismaスキーマ上の命名・命令のコツ
Prismaスキーマでは、次のような小さな工夫が長期的な開発効率に繋がります。
- モデル名は単数形で書く(例:
User
,Post
,Comment
) - createdAt, updatedAt を標準で持たせる
- リレーションフィールドには複数形を使う(例:
User.posts
) - optionalなフィールド(?)と必須フィールドを明確に分ける
これらを意識することで、スキーマファイル自体がドキュメントとしても機能し、
後からプロジェクトに参加するメンバーにも理解しやすいデータ構造を提供できます。
命名にまつわるよくある失敗例
一方で、モデル定義の初期段階でありがちな命名ミスとして、次のようなケースも見られます。
-
モデル名を複数形にしてしまう
例:Users
,Posts
→ PrismaClientでprisma.users.findMany()
のように違和感のあるコードになってしまいます。 -
リレーションフィールドを単数形にしてしまう
例:User.post: Post[]
→ 実態は複数なのに単数名のため、コードリーディング時に混乱を招きます。 -
フィールド名に意図が伝わらない一般名詞を使う
例:User.data
、Post.info
→ 「何を指すのか」が不明瞭で、後からスキーマを読む負担が増えます。
このようなミスを防ぐためにも、モデル単位では単数形、リスト型のリレーションでは複数形、フィールド名はできるだけ具体的に、を意識して設計を進めることが大切です。
リレーションの定義と注意点
複数のモデルが登場するアプリケーションでは、それぞれのモデル同士をどのように結びつけるか(リレーション設計)が非常に重要になります。
ここでは、Prismaでリレーションを定義する基本と、設計時に注意すべきポイントについて整理していきます。
@relation
と fields
, references
の書き方
Prismaでは、リレーションを定義する際に @relation
属性を使います。
たとえば、Post
モデルが User
モデルに紐づく例は次のように書きます。
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
ここでのポイントは、
Post
側が 外部キー(authorId
)を持つ@relation(fields: [...], references: [...])
で どのカラム同士が繋がるか明示する
という点です。
Prismaでは、このfields
とreferences
の明示がとても大切です。
これを省略すると、意図しないリレーションが組まれてしまったり、マイグレーションエラーが発生しやすくなります。
1対多(1:N)、1対1、N対N の設計パターン
リレーションにはいくつかのパターンがあります。
-
1対多(1:N)
例:User
が複数のPost
を持つ -
1対1(1:1)
例:User
に1つだけProfile
を紐付ける -
多対多(N:N)
例:Post
に複数のTag
が紐づき、Tag
も複数のPost
に紐づく
それぞれに適したリレーション設計が求められます。
特にN:Nの場合は、中間テーブルを自動生成するか、手動で中間モデルを作成するかを意図的に選択することが重要です。
onDelete, onUpdate の活用例と考慮点
リレーションの設計では、親モデルが削除されたときに子モデルをどう扱うかを考えておく必要があります。
Prismaでは、@relation 属性にオプションで onDelete や onUpdate を指定できます。
例:
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
-
Cascade
親が削除されたら子も自動で削除される(例:User削除時にPostも削除) -
Restrict
子が存在する限り親を削除できない(デフォルト) -
SetNull
親が削除されたら子の外部キーをnullにする
運用方針によって適切な設定を選ぶことが大切です。
特にCascadeを使う場合、誤って親データを消すと大量の子データも消えるため、十分な注意が必要です。
Prisma特有のリレーション定義ミスあるある
Prismaでは、リレーション設定ミスによる以下のようなトラブルがよく起こります。
-
片側だけにフィールドを定義してしまう
(PostにauthorIdはあるけど、Userにpostsがない、など) -
fieldsとreferencesの指定漏れでマイグレーションエラー
-
N:Nリレーションで自動中間テーブル生成に気づかず混乱する
こういったミスを防ぐためにも、
- リレーションは必ず「両側から辿れる」形を基本とする
- 明示的なfields/references指定を欠かさない
という意識が重要になります。
次の章では、こうして定義したモデル・リレーションをどのようにマイグレーション管理していくか、そして開発〜本番フローでDBをどう扱うかについて整理していきます。
Prismaのマイグレーション戦略
モデルを設計した後は、それを実際のデータベースに反映する必要があります。
このプロセスを管理するのが、Prismaのマイグレーション機能です。
この章では、Prismaのマイグレーションコマンドの使い分けと、運用時に気をつけたいポイントを整理していきます。
migrate dev
, db push
, migrate deploy
の違いと使い分け
Prismaにはいくつかのスキーマ反映コマンドが用意されています。それぞれの役割と使い分けは次の通りです。
コマンド | 主な用途 | 特徴 |
---|---|---|
npx prisma migrate dev | ローカル開発用 | マイグレーションファイルを生成し、DBに適用。履歴管理あり |
npx prisma db push | テスト環境・PoC用 | 直接スキーマをDBに反映。履歴管理なし |
npx prisma migrate deploy | 本番用 | すでに生成されたマイグレーションを本番DBに適用 |
開発中は主に migrate dev
を使い、
テスト環境では手軽に試すために db push
を使う場面もあります。
本番環境では必ず migrate deploy
を使用し、履歴の一貫性を守る運用が推奨されます。
マイグレーション履歴(migrations/)の扱い
migrate dev
を実行すると、prisma/migrations/
ディレクトリにマイグレーションファイルが生成されます。
これらのファイルは、コードと同様にGitで管理します。
つまり、スキーマの進化履歴もソースコードの一部として扱うイメージです。
ここでの注意点は、
- マイグレーションファイルを勝手に編集しない
- 不要なマイグレーション(やり直し)は、基本的に作り直すか、明示的にリセットする
という運用ルールを持つことです。
schema.prisma の編集と git 運用の注意点
スキーマファイル(schema.prisma
)を編集したら、以下の流れを基本とします。
- スキーマ編集(フィールド追加・削除・修正など)
npx prisma migrate dev
でマイグレーションファイルを生成- 動作確認(DBが期待通り更新されているか)
- 変更内容をGitにコミット(スキーマとマイグレーション両方)
このとき、スキーマだけを変更してマイグレーションを生成しないままコミットしてしまうと、
あとから環境を再現できなくなるリスクがあります。
そのため、「schema.prismaを修正したら必ずmigrateする」という習慣を意識して運用していきます。
PrismaClientのインスタンス設計をふり返る
ここでは、これまでに導入してきたPrismaClientのインスタンス設計について、あらためて整理します。
PrismaClientは、データベースと直接やりとりする重要なコンポーネントですが、NestJSのようなフレームワークと統合する際には、ライフサイクル管理(接続・切断)を意識した設計が求められます。
DI対応のポイントと PrismaService の構成
NestJSでは、依存関係の注入(DI: Dependency Injection)を活用して各コンポーネントを疎結合に保ちます。
PrismaClientも例外ではなく、サービスクラス(PrismaService)としてDI対応させるのが基本です。
作成した PrismaService は次のような形になっています。
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect(); // アプリ起動時にDB接続
}
async onModuleDestroy() {
await this.$disconnect(); // アプリ終了時にDB切断
}
}
この構成にすることで、
- アプリケーションの起動時に自動でDBに接続
- シャットダウン時にDB接続を安全に閉じる
といったライフサイクル管理が自動化されます。
PrismaModuleでの再利用設計
PrismaServiceを各コンポーネントで使い回すために、PrismaModuleを作成し、
NestJSのimports
に登録して利用できるようにしました。
PrismaModuleはとてもシンプルで、次のようになっています。
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
これによって、たとえば PostsModule
側ではこうインポートできます。
@Module({
imports: [PrismaModule],
providers: [PostsService],
})
export class PostsModule {}
この設計により、
- PrismaServiceを複数のモジュール間で安全に共有できる
- PrismaServiceの実装が変更されても、依存先を意識せずに差し替えできる
という柔軟性の高い構成が実現できました。
onModuleInit / onModuleDestroy による接続制御
PrismaService内で OnModuleInit と OnModuleDestroy を実装したことによって、
- アプリ起動後すぐにDB接続する
- アプリ停止時に接続をクリーンに閉じる
という動きが保証されています。
これにより、たとえばE2Eテストでアプリを立ち上げたり終了させたりするたびに、不要なコネクションが残り続ける問題を防げるようになっています。
小さな仕組みですが、これがあるだけでシステム全体の安定性と運用のしやすさが大きく向上します。
環境変数とDB接続の切り替え
開発環境・テスト環境・本番環境など、アプリケーションが動作する環境によって、
接続するデータベースや使用する設定を適切に切り替える必要があります。
この章では、環境ごとにDB接続先を分ける方法と、NestJSとPrismaでの実践的な切り替え方を整理します。
E2E / CI環境との接続先の分離とその意義
テスト環境(特にE2EテストやCI環境)では、「開発用DBを壊してしまう」「本番データにアクセスしてしまう」などの事故を防ぐために、専用のテストDBにだけ接続する構成を整えた方がいいかと思います。
これによって、
- テスト失敗時にデータをリセットできる
- テストが他の環境に影響を与えない
- 本番・開発環境を安全に守れる
といったメリットが得られます。
一見地味な設計に見えますが、長期運用を考えると非常に大きな意味を持ちます。
テスト環境でのマイグレーションと初期化
テスト用のデータベースを用意したら、次はそのデータベースに必要なスキーマを適用し、テストのたびにクリーンな状態を維持する仕組みを整えます。
この章では、テスト環境でのマイグレーションの適用方法と、テスト前にデータベースを初期化する実践的な流れについて整理します。
db push
or migrate deploy
のタイミング
テスト環境のスキーマ整備には、状況に応じて次の2つのコマンドを使い分けます。
コマンド | 主な用途 | 特徴 |
---|---|---|
npx prisma db push |
E2Eテスト前の手軽なスキーマ適用 | マイグレーションファイルを経由せず、直接DBに適用 |
npx prisma migrate deploy |
本番運用を意識したスキーマ適用 | 生成済みマイグレーションファイルを順に適用 |
E2Eテストでは、素早くスキーマを反映したい場面が多いため、db push
を使う場面が多くなります。
一方で、CI/CDパイプラインや本番環境では、履歴一貫性を保つために migrate deploy
を使う運用が基本となります。
テスト前にデータベースをリセットする
テスト環境では、テスト実行のたびにデータベースをクリーンな状態に戻すことが非常に重要です。
一例として、test/utils/db.ts
などにリセット用のヘルパー関数を用意しておきます。
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function resetDatabase() {
await prisma.post.deleteMany();
// 必要に応じて他のテーブルもリセット
}
テストコード内では、beforeAll
でこれを呼び出します。
beforeAll(async () => {
await resetDatabase();
});
これにより、
- どのテストも独立したクリーンな状態で始まる
- テストの結果に前のテストの副作用が影響しない
という、健全なテスト環境が作られます。
初期データ(seed)の扱いとテストでの注意点
テストケースによっては、データベースが空の状態ではなく、ある程度の初期データが存在していることを前提にテストを行いたい場合があります。
たとえば、次のような状況です。
- 記事一覧を取得するテストをしたいので、事前に数件の記事が登録されている必要がある
- ユーザー認証が必要なテストでは、事前にテスト用ユーザーアカウントを作成しておく必要がある
こういった場合、テストのセットアップ時にテスト用の最小限のデータを作成します。
具体例:テスト用の初期データを挿入する
たとえば、resetDatabase()
関数の中で、リセット後に初期データを作成します。
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function resetDatabase() {
// 全テーブルをリセット
await prisma.post.deleteMany();
await prisma.user.deleteMany();
// 必要な初期データを作成
const user = await prisma.user.create({
data: {
email: 'test@example.com',
name: 'Test User',
},
});
await prisma.post.createMany({
data: [
{ title: 'First Post', content: 'Content 1', authorId: user.id },
{ title: 'Second Post', content: 'Content 2', authorId: user.id },
],
});
}
このようにしておくと、どのテストも「特定のユーザーと複数の記事が存在している状態」から確実にスタートできるため、毎回のテスト実行における前提条件がブレなくなります。
✅ 初期データ設計で意識するポイント
-
必要最低限のデータだけを作成する
テスト対象に無関係なデータを大量に作ると、テストが遅くなったり、失敗原因がわかりにくくなります。 -
「何が存在しているか」を常に明示できる状態にする
テストコード側で「id: 1のユーザーがいる前提」などが自然に読める構成にします。 -
reset
→seed
を常にセットで動かす設計にする
テスト間の副作用を完全に排除し、テストごとに独立した世界を作ります。
この初期データ管理の意識ができていると、テストの読みやすさ、メンテナンスのしやすさ、失敗時のデバッグ容易さが大きく向上します。
テストの信頼性を支える、地味ですが非常に大切な部分です。
おわりに
ここまでで、NestJSとPrismaを組み合わせた開発において、
より実践的なデータベース設計・リレーション定義・マイグレーション運用の基本を一通り整理してきました。
単に動くAPIを作るだけでなく、
- モデル同士の関係性を丁寧に設計すること
- PrismaClientをNestJSと自然に統合すること
- 開発環境・テスト環境・本番環境ごとに接続先を切り替えること
- テスト環境ではクリーンな状態を維持すること
といった「運用を意識した設計力」を少しずつ積み上げることができたと感じています。
今回取り上げた内容は、一つ一つは地味な作業に見えるかもしれません。
しかし、これらを丁寧に積み重ねておくことが、プロジェクトが成長したときの大きな差につながります。
次回は、いよいよフロントエンド(React)との接続と、本番環境へのデプロイ構成に進んでいきます。
APIだけでなく、実際にブラウザから触れる形にしていく過程を一緒に進めていきましょう。
関連する技術ブログ
チャットアプリ(画像・PDF送信、ビデオ通話機能付き)
お客様固有の要件を除き一般的なチャットアプリに求められる最低限の機能を実装しデモアプリとしてご紹介いたします。
shinagawa-web.com
管理ダッシュボード機能(グラフ表示、データ取り込み)
一般的な家計簿アプリとして求められる最低限の機能を実装しデモアプリとしてご紹介いたします。
shinagawa-web.com
NestJSで記事投稿APIを作ろう ─ Prisma導入とCRUD実装の基本
NestJSとPrismaを使って、シンプルな記事投稿APIを一から作っていく手順をまとめました。開発環境の整え方から、データベースとの接続、APIの作成、データのチェック(バリデーション)まで、基本を一歩ずつ丁寧に解説しています。
shinagawa-web.com
NestJSアプリの信頼性を高める ─ ログ・エラーハンドリング・テスト戦略
NestJSアプリを本番品質へと近づけるために、ロギング・例外処理の共通化・ユニットテストとE2Eテストの構築までを丁寧に解説。実践的なコードと構成例を通じて、信頼されるAPI設計の基礎を整えます。
shinagawa-web.com
NestJS × React × Railway:ブログUIを実装して本番環境へデプロイ
NestJS+React+Prismaで構成したブログアプリを、Dockerで本番ビルドし、クラウドサービス「Railway」へデプロイするまでの手順をまとめました。本番DB接続、環境変数管理、フロントエンドとバックエンドの統合配信など、運用を意識した実践的な構成に仕上げていきます。個人開発やPoCにも応用できる「即戦力の本番構成」を一緒に組み立てていきましょう。
shinagawa-web.com
Next.jsとAuth.jsで認証機能を実装するチュートリアル
Next.jsでアプリケーションを作る時に必要となる認証機能をどのように実装するかをご紹介する記事となります。アカウント登録から始まり、ログイン、ログアウト、ページごとのアクセス制御、OAuth、二要素認証、パスワードリセットなど認証に関連する様々な機能をコードベースでご紹介します。
shinagawa-web.com
Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
Next.jsを活用したメール認証の実装方法を解説。アカウント登録時のトークン発行から、Sendgridを使ったメール送信処理まで、具体的な手順を紹介します。
shinagawa-web.com
ユーザー向けパスワードリセット機能の実装方法:トークン発行からメール送信、セキュリティ対策まで
このガイドでは、ユーザーのパスワードリセット機能を実装するためのステップを詳しく解説します。トークンの生成、リセットリンクのメール送信、そしてセキュリティ対策まで、効果的な実装方法を紹介します。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための技術支援
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ