Next.js: データベース統合: Prisma

最終更新:2026-08-26

Prisma は Node.js エコシステムで最も人気のある ORM です。TypeScript でデータモデルを記述し、型安全なデータベースクライアントを自動生成できます。

1. 学習目標



2. フルスタックエンジニアの実話

(1) 課題: 生 SQL だけでは開発効率が大幅に低下する

Bob は TaskFlow チームのテックリードです。チームはネイティブ SQL で PostgreSQL を操作しています:

「各 API エンドポイントに 20 行の SQL テンプレートコードを書かなければなりません。JOIN を使うとクエリでフィールドを見落としやすく、テーブル構造を変更するたびにマイグレーションスクリプトを手動で保守する必要があります。最も厄介なのは、TypeScript の型がデータベースのフィールドと同期されておらず、実行時にしかカラム名の間違いに気づけないことです。」

問題 週あたりの所要時間 影響
手書き SQL テンプレート 8h 繰り返し作業
型の不一致のデバッグ 4h ランタイムエラー
手動マイグレーションスクリプト 3h 見落としが発生しやすい
ドキュメントが最新でない 2h 新人の習熟に時間がかかる

(2) Prisma ORM の解決策

Prisma Schema でモデルを定義 → TypeScript の型を自動生成 → 型安全な CRUD。

PRISMA
// prisma/schema.prisma — データモデルがドキュメントになる
model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}

(3) 効果

観点 手書き SQL Prisma
API CRUD 操作あたりのコード量 25 行 3 行
型安全性 ❌ 手動定義 ✅ 自動生成
マイグレーション管理 手動 SQL ファイル prisma migrate dev
開発体験 IDE/DB クライアントの切り替え Prisma Studio に統合
ドキュメント同期 プラグイン スキーマがドキュメント


3. Prisma のインストールとプロジェクト初期化

(1) インストール手順

BASH
# 1. Prisma CLI とクライアントをインストール
npm install prisma @prisma/client --save-dev
# または一度に
npx prisma init --datasource-provider postgresql
100%
graph LR
    A[prisma init] --> B[prisma/schema.prisma を生成]
    A --> C[.env に DATABASE_URL を生成]
    B --> D[データモデルを定義]
    D --> E[prisma migrate dev]
    E --> F[Prisma Client を生成]
    F --> G[インポートして使用]

    style A fill:#cce5ff
    style D fill:#d4edda
    style F fill:#fff3cd
生成ファイル 目的
prisma/schema.prisma データモデル定義
.env データベース接続文字列 (DATABASE_URL)

(2) クライアントシングルトンの初期化

TS
// lib/prisma.ts — グローバルシングルトン(ホットリロード時の複数接続作成を防止)
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
💡 ヒント: Next.js のホットリロードはページ更新のたびに新しい PrismaClient インスタンスを作成します。グローバルシングルトンパターンは開発環境で既存の接続を再利用し、「Too many connections」エラーを防ぎます。

▶ サンプル: データベース接続の確認

Output:

TEXT 📖 参照専用
Database operation executed successfully.
TS
// app/api/health/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET() {
  try {
    await prisma.$connect()
    return NextResponse.json({ status: 'ok', db: 'connected' })
  } catch (e) {
    return NextResponse.json({ status: 'error', message: (e as Error).message }, { status: 500 })
  }
}
💻 出力:

JSON
{ "status": "ok", "db": "connected" }

Output:

TEXT 📖 参照専用
{ "status": "ok", "db": "connected" }  ← 2つのキーを持つJSON: status ("ok") と db ("connected")


4. スキーマデータモデリング

(1) モデルのリレーションタイプ

100%
graph TB
    User -->|一対多| Post
    User -->|一対多| Comment
    Post -->|一対多| Comment
    Post -->|多対多| Tag

    subgraph User
        U1[id String @id @default(cuid())]
        U2[name String]
        U3[email String @unique]
    end
    subgraph Post
        P1[id String @id @default(cuid())]
        P2[title String]
        P3[content String?]
    end
    subgraph Comment
        C1[id String @id @default(cuid())]
        C2[body String]
    end
    subgraph Tag
        T1[id String @id @default(cuid())]
        T2[name String @unique]
    end

    style User fill:#d4edda
    style Post fill:#cce5ff
    style Comment fill:#f8d7da
    style Tag fill:#fff3cd
リレーションタイプ Prisma 構文 データベース実装
一対一 User Profile 外部キー + ユニーク
一対多 User Post[] 外部キー
多対多 Post Tag[] (暗黙的) 中間テーブル _PostToTag
自己参照 Category parentCategory 自己参照外部キー

(2) 完全な TaskFlow スキーマ

PRISMA
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  ADMIN
  EDITOR
  VIEWER
}

model User {
  id        String    @id @default(cuid())
  name      String
  email     String    @unique
  role      Role      @default(VIEWER)
  password  String?
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Post {
  id        String    @id @default(cuid())
  title     String
  content   String?
  published Boolean   @default(false)
  author    User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId  String
  tags      Tag[]
  comments  Comment[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@index([authorId])
}

model Comment {
  id        String   @id @default(cuid())
  body      String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    String
  author    String
  createdAt DateTime @default(now())

  @@index([postId])
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]

  @@index([name])
}

▶ サンプル: Prisma Studio での可視化操作

BASH
# Prisma Studio を起動(ブラウザ GUI でデータの表示/編集)
npx prisma studio
TEXT 📖 参照専用
1. `npx prisma studio` を実行
2. ブラウザで http://localhost:5555 を開く
3. 左側から User / Post / Comment / Tag テーブルを選択
4. 「Add Record」をクリックしてテストデータを追加
5. 「Save Changes」をクリックして保存
💡 ヒント: Studio は関連データの直接編集をサポートしています。User を編集すると、その Posts と Comments を表示できます。



5. データベースマイグレーション

(1) マイグレーションワークフロー

ステップ コマンド 機能
スキーマ変更 schema.prisma を編集 モデル/フィールドの追加・削除
マイグレーション作成 npx prisma migrate dev --name add_user_role SQL マイグレーションファイルを生成
マイグレーション適用 自動 データベース構造を更新
データベースリセット npx prisma migrate reset データをクリア + 再マイグレーション
クライアント生成 npx prisma generate TypeScript の型を更新

(2) マイグレーションファイル構造

BASH
prisma/migrations/
├── 20260706000001_init/
│   └── migration.sql          # 初期テーブル作成
├── 20260706000002_add_user_role/
│   └── migration.sql          # ALTER TABLE で role カラムを追加
└── migration_lock.toml        # データベースプロバイダのロック

▶ サンプル: Role のマイグレーション追加

BASH
npx prisma migrate dev --name add_role_enum

生成された SQL:

SQL
-- prisma/migrations/20260706000002_add_role_enum/migration.sql
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'EDITOR', 'VIEWER');

-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'VIEWER';

Output:

TEXT 📖 参照専用
Statement(s) executed successfully.
💡 ヒント: prisma migrate dev はスキーマの変更を自動検出し、対応する SQL 文を生成します。ALTER TABLE 文を手動で書く必要はありません。



6. ハンズオン CRUD 操作

(1) サーバーコンポーネントでのデータ読み取り

TSX
// app/posts/page.tsx — RSC がデータベースを直接クエリ
import { prisma } from '@/lib/prisma'

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: { author: { select: { name: true } }, tags: true },
    orderBy: { createdAt: 'desc' },
    take: 20
  })

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>著者: {post.author.name}</p>
          <p>{post.tags.map((t) => t.name).join(', ')}</p>
        </article>
      ))}
    </div>
  )
}

(2) Server Action: データ書き込み

TS
// app/actions/posts.ts — Server Action CRUD
'use server'

import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(data: { title: string; content?: string; authorId: string }) {
  const post = await prisma.post.create({ data })
  revalidatePath('/posts')
  redirect(`/posts/${post.id}`)
}

export async function deletePost(id: string) {
  await prisma.post.delete({ where: { id } })
  revalidatePath('/posts')
}

▶ サンプル: フォームと Server Action を使った記事作成

Output:

TEXT 📖 参照専用
Server action executes and calls revalidatePath() to refresh the page cache.
TSX
// app/posts/new/page.tsx
import { createPost } from '@/app/actions/posts'
import { auth } from '@/auth'

export default async function NewPostPage() {
  const session = await auth()
  if (!session?.user) return <p>先にログインしてください</p>

  return (
    <form action={createPost}>
      <input name="title" placeholder="記事タイトル" required />
      <textarea name="content" placeholder="記事内容" rows={10} />
      <input type="hidden" name="authorId" value={session.user.id} />
      <button type="submit">公開</button>
    </form>
  )
}

Output:

TEXT 📖 参照専用
A form with input fields and submit button.
Visible text: 公開
TS
// 修正版 Server Action: FormData を受け取る
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const authorId = formData.get('authorId') as string

  if (!title || !authorId) throw new Error('必須フィールドが不足しています')

  const post = await prisma.post.create({
    data: { title, content, authorId }
  })
  revalidatePath('/posts')
  redirect(`/posts/${post.id}`)
}

(3) Route Handler: 完全な CRUD

TS
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET() {
  const posts = await prisma.post.findMany({
    include: { author: true, comments: true },
    orderBy: { createdAt: 'desc' }
  })
  return NextResponse.json(posts)
}

export async function POST(req: Request) {
  const body = await req.json()
  const post = await prisma.post.create({ data: body })
  return NextResponse.json(post, { status: 201 })
}
TS
// app/api/posts/[id]/route.ts
import { prisma } from '@/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const post = await prisma.post.findUnique({
    where: { id },
    include: { comments: true, tags: true }
  })
  if (!post) return NextResponse.json({ error: '見つかりません' }, { status: 404 })
  return NextResponse.json(post)
}

export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const body = await req.json()
  const post = await prisma.post.update({ where: { id }, data: body })
  return NextResponse.json(post)
}

export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  await prisma.post.delete({ where: { id } })
  return NextResponse.json({ deleted: true })
}


7. 接続プール管理

(1) 接続プールの設定

設定オプション デフォルト値 本番環境推奨値 説明
connection_limit 10 20–50 最大同時接続数
pool_timeout 10s 30s 接続タイムアウト
idle_timeout 10s 30s アイドル接続の保持時間

(2) 接続プールパラメータの設定

TS
// lib/prisma.ts — 本番レベルの接続プール
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
  datasources: {
    db: {
      url: process.env.DATABASE_URL + '?connection_limit=20&pool_timeout=30&idle_timeout=30'
    }
  }
})

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
⚠️ 注意: サーバーレス環境(Vercel)では、データベース接続数が制限されています。Prisma Accelerate(接続プールプロキシ)または Supabase の接続プール(?pgbouncer=true)の使用をお勧めします。

▶ サンプル: Supabase 接続プール設定

ENV
# .env — Supabase 接続プール
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
TS
// サーバーレス最適化: リクエストごとにインスタンスを再利用
import { PrismaClient } from '@prisma/client'

let prisma: PrismaClient

export function getPrisma() {
  if (!prisma) {
    prisma = new PrismaClient({
      datasources: { db: { url: process.env.DATABASE_URL } }
    })
  }
  return prisma
}

Output:

TEXT 📖 参照専用
Database operation executed successfully.


8. 完全なサンプル: ブログシステム CRUD の総合実装

TSX
// app/posts/[id]/page.tsx — 記事詳細 + コメント機能
import { prisma } from '@/lib/prisma'
import { notFound } from 'next/navigation'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

async function addComment(formData: FormData) {
  'use server'
  const session = await auth()
  if (!session?.user) throw new Error('先にログインしてください')

  const body = formData.get('body') as string
  const postId = formData.get('postId') as string

  if (!body || !postId) throw new Error('フィールドが不足しています')

  await prisma.comment.create({
    data: { body, postId, author: session.user.name! }
  })
  revalidatePath(`/posts/${postId}`)
}

export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params

  const post = await prisma.post.findUnique({
    where: { id },
    include: {
      author: { select: { name: true } },
      tags: true,
      comments: { orderBy: { createdAt: 'desc' } }
    }
  })

  if (!post) notFound()

  return (
    <div>
      <h1>{post.title}</h1>
      <p>著者: {post.author.name}</p>
      <div>{post.content}</div>
      <p>タグ: {post.tags.map((t) => t.name).join(', ')}</p>

      <hr />
      <h2>コメント ({post.comments.length})</h2>
      {post.comments.map((c) => (
        <div key={c.id}>
          <strong>{c.author}</strong>: {c.body}
        </div>
      ))}

      <form action={addComment}>
        <input type="hidden" name="postId" value={post.id} />
        <textarea name="body" placeholder="レビューを書く..." rows={3} required />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}
💡 ヒント: このサンプルは、1 つのページで RSC のデータ読み取り(prisma.post.findUnique)と Server Action の書き込み(addComment)の両方を示しており、Next.js + Prisma のフルスタック構成を実現しています。


❓ よくある質問

Q Prisma と Drizzle ORM のどちらを選ぶべきですか?
A Prisma は初心者にとってより使いやすく(宣言的スキーマ + Studio 可視化 + 自動マイグレーション)、Drizzle は SQL 構文により近く、わずかに優れたパフォーマンスを提供します。このチュートリアルでは Prisma を使用します。最大のエコシステム(43k⭐)と包括的なドキュメントを持つためです。
Q なぜグローバルシングルトンパターンで PrismaClient を作成するのですか?
A Next.js の開発モードでは、ホットリロードによって頻繁に新しいインスタンスが作成され、データベース接続数が急増する可能性があります。グローバルシングルトンは globalThis にインスタンスをキャッシュし、PrismaClient が 1 つだけ作成されることを保証します。
Q prisma migrate devprisma db push の違いは何ですか?
A migrate dev は追跡可能な SQL マイグレーションファイルを生成します(チーム協業に適しています)。db push はスキーマをデータベースに直接同期します(迅速なプロトタイピングに適していますが、履歴は保持されません)。本番環境では migrate deploy を使用する必要があります。
Q サーバーコンポーネントからデータベースを直接クエリするとパフォーマンスの問題が発生しますか?
A いいえ。RSC はサーバー上で実行されるため、データベースを直接クエリすると API ルートを経由する場合と比べて HTTP ラウンドトリップが 1 回省略されます。Next.js のデータキャッシング(fetch の自動キャッシュ)と組み合わせるとさらに効果的です。
Q データベース操作がトランザクションであることを保証するにはどうすればよいですか?
A Prisma はネストされた書き込み(create: { post: { create: {...} } })をサポートしており、自動的にトランザクションをラップします。明示的にトランザクションを定義する必要がある場合は、prisma.$transaction([...]) または prisma.$transaction(async (tx) => {...}) を使用します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐): 既存のプロジェクトで Prisma を初期化し、UserProfile モデルの一対一リレーションを定義し、マイグレーションを実行し、レコードを挿入して Prisma Studio で検証してください。

  2. 応用問題 (⭐⭐): 記事システムの完全な CRUD API ルートを実装してください。GET(一覧 + ページネーション)、POST(作成)、PATCH(更新)、DELETE(削除)のすべてを Prisma 操作で実装します。

  3. 発展問題 (⭐⭐⭐): Prisma の $transaction を使用して、記事を作成すると同時にタグを関連付ける機能を実装してください。タグが存在しない場合は先にタグを作成し、その後記事とタグの多対多リレーションを確立します。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%