言語を切り替える
テーマを切り替える

Next.js + Prisma 完全入門ガイド:設定から実践まで(接続リーク対策付き)

ターミナルに、またあのイライラする赤文字が出た:Error: Can't reach database server at localhost:5432。いや、もっと正確には:FATAL: sorry, too many clients already

DB は動いている。接続文字列も合っている。なのに接続できない。プロジェクト起動直後は問題なかったのに、コードを何度か直したらエラーが出始める。Next.js 開発サーバーを再起動? 効かない。DB を再起動? 一時的に直っても、またすぐ落ちる。

後から分かった。これは Next.js + Prisma 開発で最も古典的な落とし穴のひとつ:ホットリロードによる DB 接続リークだ。要するに、コードを変えるたびに Next.js のホットリロードが新しい Prisma インスタンスを作るのに、古い接続は自動で閉じられず、最終的に接続プールがパンクする。

同じような経験はないだろうか。Next.js に DB を足したい。Prisma、TypeORM、Drizzle で悩む。Prisma を選んで設定したら、あちこちでエラー。Schema で 1 対多・多対多が書けない。あるいは開発の途中で、突然 DB が止まる。

実は Prisma の学習曲線はそれほど急ではない。設定も複雑ではない。大事なのは、環境の組み方、接続リークの避け方、Schema の設計、CRUD の書き方——この核心だけ押さえることだ。本記事はそこを整理し、遠回りを減らすためのものです。

なぜ Prisma を選ぶのか?(TypeORM・生 SQL との比較)

Next.js の DB 案はいろいろある。TypeORM は老舗で安定、Drizzle は軽量な新鋭、生 SQL は性能最重視。それでも Prisma を使う理由は何か。

型安全——本当に安全

Prisma が自動生成する TypeScript 型を初めて見たときは、正直びっくりした。「まあまあ」ではなく、「こういうこともできるのか」という感じ。

Schema を書いて npx prisma generate を走らせると、全モデルの型定義が出る。単なる interface ではなく、インテリセンス付きの完全な型prisma.user.findUnique({ where: { id: と書くと、VSCode が id の型や where に使えるフィールドを教えてくれる。

比較すると:

  • TypeORM@Entity() @Column() などのデコレータを自分で書く。型定義と DB schema が分離し、ズレやすい
  • 生 SQL:結果は any になりがち。interface を手書きし、テーブル変更のたびに型も更新
  • Prisma:Schema が唯一の正。型は自動同期。Schema を変えたら generate し直すだけ

これは便利さの話だけではない。型安全は、実行時に爆発する前にコーディング中にミスを見つけるということだ。

開発体験——細部に差が出る

Prisma で特に気持ちよい点:

Prisma Studio:可視化 DB 管理 UI。npx prisma studio でブラウザからデータの閲覧・編集ができる。TablePlus を入れたり SQL を書いたりしなくてよい。開発がかなり楽になる。

マイグレーションprisma migrate dev でマイグレーションファイル生成と適用が一度にできる。TypeORM よりずっとシンプル。Schema を変えると変更を検知し、マイグレーション生成を聞いてくる。SQL マイグレーションを手書きしたり、順序を間違えたりする心配が減る。

クエリ構文:JavaScript の感覚に近い。

const users = await prisma.user.findMany({
  where: { email: { contains: '@gmail.com' } },
  include: { posts: true },
  orderBy: { createdAt: 'desc' }
})

直感的だろう。SQL を書かず、デコレータを覚えず、普通のオブジェクトとメソッド呼び出しで済む。

TypeORM の QueryBuilder と比べると:

const users = await userRepository.createQueryBuilder("user")
  .where("user.email LIKE :email", { email: "%@gmail.com%" })
  .leftJoinAndSelect("user.posts", "posts")
  .orderBy("user.createdAt", "DESC")
  .getMany()

同じことでも、Prisma の方が読みやすく、ミスも入りにくい。

パフォーマンスとエコシステム

「ORM は遅い。本番は生 SQL では?」——という声もある。

正直、誤解の余地がある。Prisma にオーバーヘッドはあるが、多くのプロジェクトでは十分許容できる。最適化もされている:

  • N+1 の自動対策include で関連取得するとき、クエリをまとめて重複リクエストを避ける
  • 接続プール管理:デフォルトで num_cpus * 2 + 1 など合理的
  • クエリ最適化:必要なフィールドだけ。select で転送量をさらに削減

本当にボトルネックになったら、生 SQL も使える:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}`

ORM の便利さと、要所での生 SQL——両方取れる。

エコシステム面では、GitHub で 38k+ star、Vercel 公式推奨、ドキュメントも充実。Next.js 公式ドキュメントにも Prisma 連携の章がある。主流の選択肢と言ってよい。

Prisma が完璧というわけではない。ただ多くの Next.js フルスタック案件では、型安全・開発体験・エコシステムの三つで十分な理由になる。

環境構築:Prisma をゼロから設定する

Prisma を選んだら、次は環境構築。10 分ほどで終わる。

依存関係のインストールと初期化

Next.js プロジェクトがある前提。なければ npx create-next-app@latest から。

Prisma のインストール:

npm install prisma @prisma/client

2 つのパッケージ:

  • prisma:CLI(初期化、マイグレーション、Studio)
  • @prisma/client:実際に DB を叩くクライアント

インストール後の初期化:

npx prisma init

このコマンドは:

  1. prisma/schema.prisma を作成(中核設定)
  2. .env を作成し、DATABASE_URL を置く

データベース接続の設定

.env を開くと:

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

PostgreSQL の接続文字列の例。形式は:

postgresql://ユーザー名:パスワード@ホスト:ポート/DB名?schema=public

ローカル開発は PostgreSQL を推奨。機能が豊富で Prisma のサポートも良く、本番も PostgreSQL になりがちだから。

PostgreSQL が未インストールなら、Docker が最速:

docker-compose.yml を作成:

version: '3.8'
services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

起動:

docker-compose up -d

DB が立ち上がる。.env を更新:

DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"

重要.env は Git にコミットしない。.gitignore に入っているか確認:

.env
.env.local

さもないとパスワードが漏れる。

MySQL や SQLite を使う場合、接続文字列の形式が少し違う:

MySQL

DATABASE_URL="mysql://root:password@localhost:3306/mydb"

SQLite(ローカルファイル DB。小規模向け):

DATABASE_URL="file:./dev.db"

Prisma Client の生成

接続を設定したら、クライアントを生成:

npx prisma generate

schema.prisma を読み、TypeScript の型とクエリメソッドを node_modules/@prisma/client に生成する。

Schema を変えるたびに npx prisma generate が必要。しないと TypeScript が型不一致で怒る。

ここまでで環境は完了。流れは:パッケージ → 初期化 → DB 設定 → クライアント生成。難しくはない。

次はホットリロード接続リーク——多くの人がハマるポイントだ。

ホットリロード接続リークの解決(重点)

いよいよ、多くの人を苦しめる問題:FATAL: sorry, too many clients already

何が起きているのか

Next.js 開発モードにはホットリロード(Hot Module Replacement, HMR)がある。コードを保存すると自動でページが更新され、サーバー再起動が不要。とても便利。

しかし Prisma と組み合わせると問題が出る。

コードを変えるたびに Next.js はモジュールを再読み込みする。API ルートや Server Component で直接 new PrismaClient() していると、ホットリロードのたびに新しい Prisma インスタンスができる。

新インスタンスは新しい DB 接続を開く。古い接続は自動では閉じない——そのまま残る。

PostgreSQL のデフォルト最大接続は 100(変更可能だが、通常はそのままでよい)。コードを数回直すと 10、20、50、100……満杯になり、新規接続を拒否して too many clients already になる。

初めて遭遇したときは全く腑に落ちなかった。DB は正常なのに、なぜ接続できない? 開発サーバー再起動で一時回復し、またすぐ落ちる。後で Prisma の GitHub Issues を見ると、Issue #10247 などで同じ不満が山ほどあった。

シングルトンパターン——一発で解決

対策はシンプル:シングルトンで、アプリ全体で PrismaClient を 1 回だけ作る

具体的には、インスタンスを globalThis に置く。globalThis は JS のグローバルオブジェクトで、ホットリロードでも消えない。初回作成後は再利用され、新しい接続は増えない。

コード例:

プロジェクトルートに lib/prisma.ts を作成:

import { PrismaClient } from '@prisma/client'

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

export const prisma = globalForPrisma.prisma || new PrismaClient({
  log: ['query', 'error', 'warn'], // 开发时可以看到所有查询
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

説明:

  1. globalForPrismaglobalThis に型を付けたもの
  2. export const prisma = globalForPrisma.prisma || new PrismaClient() が核心。既にあればそれを使い、なければ新規作成
  3. if (process.env.NODE_ENV !== 'production') は開発環境だけ globalThis に保存。本番はデプロイごとに新規で、ホットリロードはない

以降はすべてこのファイルから prisma を import:

App Router(Next.js 13+) の Server Component:

// app/users/page.tsx
import { prisma } from '@/lib/prisma'

export default async function UsersPage() {
  const users = await prisma.user.findMany()
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

API Route

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

export async function GET() {
  const users = await prisma.user.findMany()
  return NextResponse.json(users)
}

Pages Router(Next.js 12 以前) の API:

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const users = await prisma.user.findMany()
  res.status(200).json(users)
}

ポイント:ファイル内で直接 new PrismaClient() しない。常に lib/prisma.ts から import

本番環境は心配不要

本番(Vercel など)でも同じが必要?——いいえ。

本番はリクエストごとに独立し、ホットリロードはない。ビルド済み静的ファイルで、頻繁なモジュール再読み込みもない。

だから if (process.env.NODE_ENV !== 'production') で、開発はシングルトン、本番は通常作成になる。

Prisma 公式も推奨しており、ドキュメントに専用の節がある。この通りにすれば、ほぼ問題ない。

この落とし穴は本当に多くの人を苦しめたが、原理が分かれば一行レベルの話。lib/prisma.ts を用意すれば、接続リークは気にしなくてよくなる。

Schema 設計のベストプラクティス

環境と接続リークが済んだら、テーブル設計。Prisma の Schema は prisma/schema.prisma。テーブル構造とリレーションはすべてここ。

基本モデル定義

ブログの User と Post の例:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

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

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  password  String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

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

要点:

  • @id:主キー
  • @default(autoincrement()):自動採番 ID
  • @unique:一意制約(email の重複不可など)
  • String?? は nullable
  • @default(now()):作成時に現在時刻
  • @updatedAt:更新のたびに時刻を更新

命名規則:

  • モデル名は PascalCase:User, Post, UserProfile
  • フィールドは camelCase:createdAt, authorId
  • DB テーブル名は自動で小文字複数形:UserusersPostposts

リレーション設計:1 対多・多対多・1 対 1

リレーションは Schema の核心。初心者が迷いやすいので、ひとつずつ。

1 対多(One-to-Many)

1 ユーザーが複数記事、1 記事は 1 ユーザー——典型的な 1 対多。

上の User / Post がそれ:

  • Userposts Post[](1 ユーザーに複数 Post)
  • Postauthor UserauthorId Int(1 Post は 1 User に属する)

@relation(fields: [authorId], references: [id]) は外部キー定義:

  • fields: [authorId]:現在モデル(Post)の authorId
  • references: [id]:関連先(User)の id

多対多(Many-to-Many)

1 記事に複数タグ、1 タグが複数記事——多対多。

Prisma には暗黙と明示の 2 通り。

暗黙関連(シンプル。Prisma が中間テーブルを自動管理):

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

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

中間テーブル _PostToTag が自動作成される。クエリは include: { tags: true } でタグも取得。

明示的中間テーブル(柔軟。中間に追加フィールド可能):

model Post {
  id       Int        @id @default(autoincrement())
  title    String
  postTags PostTag[]
}

model Tag {
  id       Int        @id @default(autoincrement())
  name     String
  postTags PostTag[]
}

model PostTag {
  id        Int      @id @default(autoincrement())
  postId    Int
  tagId     Int
  post      Post     @relation(fields: [postId], references: [id])
  tag       Tag      @relation(fields: [tagId], references: [id])
  createdAt DateTime @default(now())
  
  @@unique([postId, tagId]) // 防止重复关联
}

PostTagcreatedAt などを足せる。追加フィールドが不要なら暗黙の方が楽。

1 対 1(One-to-One)

1 ユーザーに 1 プロフィール、1 プロフィールは 1 ユーザー。

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

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String?
  userId Int    @unique
  user   User   @relation(fields: [userId], references: [id])
}

userId@unique を付け、1 User に 1 Profile だけにする。

応用テクニック

列挙型(Enum)

固定値だけのフィールドには enum:

enum Role {
  USER
  ADMIN
  MODERATOR
}

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

複合ユニークインデックス

複数フィールドの組み合わせで一意:

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  authorId Int
  slug     String
  
  @@unique([authorId, slug]) // 同一个作者的 slug 不能重复
}

リレーションの曖昧さ解消

2 モデル間に複数リレーションがあるときは name が必要:

model User {
  id             Int    @id @default(autoincrement())
  writtenPosts   Post[] @relation("PostAuthor")
  favoritePosts  Post[] @relation("PostFavorites")
}

model Post {
  id          Int    @id @default(autoincrement())
  title       String
  authorId    Int
  author      User   @relation("PostAuthor", fields: [authorId], references: [id])
  favoritedBy User[] @relation("PostFavorites")
}

name がないと Prisma はどれがどれか判断できずエラーになる。

完全例:ブログシステム

上記をまとめたブログ Schema:

generator client {
  provider = "prisma-client-js"
}

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

enum Role {
  USER
  ADMIN
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String?
  password  String
  role      Role      @default(USER)
  posts     Post[]
  comments  Comment[]
  profile   Profile?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  avatar String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id])
}

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

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id])
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

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

Schema ができたら npx prisma migrate dev --name init でマイグレーション生成・適用。テーブル、インデックス、外部キーが自動で作られる。

Schema 設計は、1 対多・多対多・1 対 1 と @relation を押さえ、enum やユニーク制約を足せば、多くの案件で足りる。

CRUD 操作の実践

Schema ができたらデータ操作。Prisma の API は直感的で、見ればだいたい分かる。

すべて lib/prisma.ts から prisma を import すること。自分で new PrismaClient() しない。

データ作成(Create)

1 件作成

import { prisma } from '@/lib/prisma'

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    password: 'hashed_password_here'
  }
})

一括作成

const users = await prisma.user.createMany({
  data: [
    { email: '[email protected]', name: 'Bob', password: 'pass1' },
    { email: '[email protected]', name: 'Charlie', password: 'pass2' }
  ],
  skipDuplicates: true // 跳过已存在的 email
})

console.log(`创建了 ${users.count} 个用户`)

ネスト作成(ユーザーと同時に Profile):

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Dave',
    password: 'pass',
    profile: {
      create: {
        bio: '一个热爱编程的开发者'
      }
    }
  },
  include: {
    profile: true // 返回结果包含 profile
  }
})

データ取得(Read)

1 件取得

// 根据唯一字段查询
const user = await prisma.user.findUnique({
  where: { email: '[email protected]' }
})

// 查询第一条匹配的记录
const firstPost = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' }
})

複数件取得

const users = await prisma.user.findMany({
  where: {
    email: {
      contains: '@gmail.com' // email 包含 @gmail.com
    }
  },
  orderBy: { createdAt: 'desc' },
  take: 10, // 只取 10 条
  skip: 0   // 跳过 0 条(分页用)
})

関連クエリ

include で関連データ:

const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true,      // 包含用户的所有文章
    profile: true     // 包含用户的 profile
  }
})

select で必要フィールドだけ(性能向き):

const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: {
    id: true,
    email: true,
    posts: {
      select: {
        id: true,
        title: true
      }
    }
  }
})
// 结果只包含 id, email, posts(只有 id 和 title)

include は全フィールド、select は指定のみ。データ量が大きいときは select で帯域を節約。

フィルタ条件

Prisma は多彩なフィルタに対応:

const posts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: 'Next.js' } },
      { content: { contains: 'Prisma' } }
    ],
    AND: [
      { published: true },
      { authorId: { not: 1 } } // 排除作者 ID 为 1 的
    ]
  }
})

よく使う演算子:

  • equals:等しい
  • not:等しくない
  • in:配列に含む(in: [1, 2, 3]
  • notIn:配列に含まない
  • contains:部分一致(文字列)
  • startsWith:で始まる
  • endsWith:で終わる
  • gt / gte:より大きい / 以上
  • lt / lte:より小さい / 以下

データ更新(Update)

1 件更新

const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Alice Updated' }
})

複数件更新

const result = await prisma.user.updateMany({
  where: { email: { contains: '@gmail.com' } },
  data: { role: 'ADMIN' }
})

console.log(`更新了 ${result.count} 个用户`)

Upsert(あれば更新、なければ作成):

const user = await prisma.user.upsert({
  where: { email: '[email protected]' },
  update: { name: 'Alice Updated' },
  create: {
    email: '[email protected]',
    name: 'Alice',
    password: 'pass'
  }
})

先に find して分岐する必要がなく、実務でよく使う。

データ削除(Delete)

1 件削除

const user = await prisma.user.delete({
  where: { id: 1 }
})

複数件削除

const result = await prisma.user.deleteMany({
  where: {
    createdAt: {
      lt: new Date('2023-01-01') // 删除 2023 年之前创建的用户
    }
  }
})

console.log(`删除了 ${result.count} 个用户`)

トランザクション

複数操作をすべて成功かすべて失敗にしたいとき——送金で A から引き、B に足す、など。

バッチトランザクション(独立した複数操作):

const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]', password: 'pass' } }),
  prisma.post.create({ data: { title: 'Test Post', authorId: 1 } })
])

両方成功するか、失敗してロールバック。

インタラクティブトランザクション(操作間に依存がある場合):

const transferMoney = await prisma.$transaction(async (tx) => {
  // 扣除 A 账户的钱
  const accountA = await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 100 } }
  })
  
  if (accountA.balance < 0) {
    throw new Error('余额不足')
  }
  
  // 增加 B 账户的钱
  const accountB = await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 100 } }
  })
  
  return { accountA, accountB }
})

途中でエラーが出れば全体ロールバック。残高は変わらない。

完全例:Next.js API Route

上記をまとめた CRUD API:

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

// GET /api/posts - 获取文章列表
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const page = parseInt(searchParams.get('page') || '1')
    const limit = parseInt(searchParams.get('limit') || '10')
    
    const posts = await prisma.post.findMany({
      where: { published: true },
      include: {
        author: {
          select: { id: true, name: true, email: true }
        },
        tags: true
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit
    })
    
    const total = await prisma.post.count({ where: { published: true } })
    
    return NextResponse.json({ posts, total, page, limit })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  }
}

// POST /api/posts - 创建文章
export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { title, content, authorId, tagIds } = body
    
    const post = await prisma.post.create({
      data: {
        title,
        content,
        authorId,
        tags: {
          connect: tagIds.map((id: number) => ({ id })) // 关联已有的标签
        }
      },
      include: { tags: true }
    })
    
    return NextResponse.json(post, { status: 201 })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
  }
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

// GET /api/posts/:id - 获取单篇文章
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const post = await prisma.post.findUnique({
      where: { id: parseInt(params.id) },
      include: {
        author: { select: { id: true, name: true } },
        tags: true,
        comments: {
          include: {
            author: { select: { id: true, name: true } }
          }
        }
      }
    })
    
    if (!post) {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 })
    }
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
  }
}

// PATCH /api/posts/:id - 更新文章
export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json()
    const post = await prisma.post.update({
      where: { id: parseInt(params.id) },
      data: body
    })
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to update post' }, { status: 500 })
  }
}

// DELETE /api/posts/:id - 删除文章
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    await prisma.post.delete({
      where: { id: parseInt(params.id) }
    })
    
    return NextResponse.json({ message: 'Post deleted' })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 })
  }
}

createfindUniquefindManyupdatedelete と、フィルタ・関連・トランザクションが押さえられれば、多くの場面に対応できる。

応用テクニックとよくある問題

基本 CRUD ができたら、実務で気をつける点——性能、マイグレーション、デバッグ。

パフォーマンス最適化

select で取得フィールドを絞る

デフォルトでは全フィールドを取る。列が多い・大きなテキストがあるときは select が有効:

// 不好:查询了所有字段,包括可能很大的 content
const posts = await prisma.post.findMany()

// 好:只查询需要的字段
const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    createdAt: true,
    author: {
      select: { name: true }
    }
  }
})

一覧では本文全文は不要なことが多い。タイトルと要約だけで十分。

N+1 問題を避ける

ORM の古典的な罠:N 件取得したあと、各件で関連を再クエリし、合計 N+1 回。

Prisma の include でまとめられる:

// 不好:N+1 查询
const users = await prisma.user.findMany()
for (const user of users) {
  user.posts = await prisma.post.findMany({ where: { authorId: user.id } })
}

// 好:一次查询搞定
const users = await prisma.user.findMany({
  include: { posts: true }
})

JOIN やバッチで賢くまとめ、DB への重複アクセスを減らす。

接続プールの設定

デフォルトのプールサイズは num_cpus * 2 + 1。多くの場合で足りるが、サーバーレス(Vercel)や高並行では調整が必要なことも。

DATABASE_URL にパラメータを追加:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5"

サーバーレスは 1 から始めて段階的に上げる。大きすぎると DB 接続を枯渇させる。

Vercel デプロイなら Prisma Accelerate も選択肢。HTTP 接続プールとグローバルキャッシュでサーバーレス向けに最適化されている。

マイグレーション管理

開発環境

Schema を変えたら prisma migrate dev

npx prisma migrate dev --name add-user-role

このコマンドは:

  1. Schema 変更を検知
  2. SQL マイグレーションファイルを生成
  3. DB に適用
  4. Prisma Client を再生成

--name は説明的に:add-user-rolecreate-post-table など。

本番環境

本番で prisma migrate dev は絶対に使わない。データ損失リスクがある。prisma migrate deploy を使う:

npx prisma migrate deploy

既存のマイグレーションだけ適用し、新規は作らない。

CI/CD ではデプロイ前に prisma migrate deploy を走らせ、DB とコードの構造を揃える。

マイグレーションのロールバック

Prisma に組み込みロールバックコマンドはない。手動対応:

  1. 履歴確認:
npx prisma migrate status
  1. 戻す必要があれば、SQL で変更を取り消すか、DB スナップショットから復元

本番はバックアップを取り、問題時にすぐ戻せるようにしておく。

デバッグのコツ

クエリログを有効化

実行 SQL を見たいとき:

// lib/prisma.ts
export const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error']
})

開発では各クエリの SQL・実行時間・パラメータが見える。

本番は error のみに絞る:

log: ['error']

ログ過多は性能にも影響する。

Prisma Studio の利用

可視化 DB 管理ツール:

npx prisma studio

ブラウザで:

  • 全テーブルとデータの閲覧
  • レコードの手動追加・編集・削除
  • リレーションの確認

開発中、SQL クライアントに切り替えなくて済む。

よくあるエラーの切り分け

P2002: Unique constraint failed
→ 一意制約違反(email 重複など)。既存データを確認。

P2025: Record not found
→ 更新・削除対象が存在しない。先に findUnique で確認。

P1001: Can't reach database server
→ 接続失敗。.envDATABASE_URL と DB 起動状態を確認。

Vercel デプロイの注意点

Vercel では package.jsonbuildprisma generate を入れる:

{
  "scripts": {
    "build": "prisma generate && next build"
  }
}

ビルドのたびに最新 Client が生成され、型と Schema が一致する。

DATABASE_URL は Vercel のプロジェクト設定で。コードに直書きしない。

PostgreSQL なら Vercel Postgres や Supabase も無料枠があり、設定が簡単。

まとめ

応用パートの核心:

  • 性能select でフィールド削減、include で N+1 回避、接続プールを適切に
  • マイグレーション:開発は migrate dev、本番は migrate deploy——混同しない
  • デバッグ:ログ、Prisma Studio、エラーコードの意味
  • デプロイ:build 前に prisma generate、環境変数を正しく

ここまで押さえれば、Prisma はかなりスムーズに使える。

結論

環境構築から CRUD、接続リークから Schema 設計まで、Next.js + Prisma の流れを一通り見てきた。

最重要ポイントを再掲:

接続リークlib/prisma.ts でシングルトン。すべてここから import。開発では必須。しないと接続はいつか枯渇する。

Schema 設計:1 対多・多対多・1 対 1 と @relation を理解。命名を統一し、enum を活用。

CRUDcreatefindManyupdatedelete と、include / select の違い。整合性には $transaction

性能とデプロイselect で転送削減、include で N+1 回避。マイグレーションは環境でコマンドを分ける。Vercel では prisma generate を忘れない。

Prisma は生 SQL より学習曲線が緩やかで、型安全と開発体験は本当に良い。完璧ではなく、オーバーヘッドや複雑クエリでは生 SQL が必要なこともある。それでも多くの Next.js フルスタック案件ではメリットが十分大きい。

次のステップ:

  • 小さな Next.js + Prisma プロジェクトを手を動かして試す
  • Prisma 公式ドキュメント で高度な機能を深掘りする
  • Prisma Studio で可視化 DB 管理を体験する
  • Prisma の GitHub Discussions でコミュニティの知見を拾う

コメントで質問や、Prisma で踏んだ坑の共有も歓迎。あなたの経験が、誰かの遠回りを減らすかもしれない。

Next.js + Prisma 完全セットアップ手順

インストールから接続リーク対策、Schema 設計、CRUD 操作までの一連のステップ

⏱️ 目安時間: 4 時間

  1. 1

    ステップ1: Prisma のインストールと初期化

    依存関係のインストール:
    • npm install prisma @prisma/client
    • npx prisma init

    初期化で作成されるもの:
    • prisma/schema.prisma:Schema 定義ファイル
    • .env:環境変数ファイル(DATABASE_URL を含む)

    DB 接続の設定:
    • .env で DATABASE_URL を設定
    • 形式:postgresql://user:password@localhost:5432/dbname
  2. 2

    ステップ2: 接続リーク問題の解決

    シングルトンパターンの作成:
    • lib/prisma.ts を作成
    • 開発環境では globalThis にインスタンスをキャッシュ
    • 本番環境ではインスタンスをそのままエクスポート

    コード:
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
    export const prisma = globalForPrisma.prisma || new PrismaClient()
    if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

    これでホットリロード時に複数接続が作られるのを防げます
  3. 3

    ステップ3: Schema の設計

    モデルの定義:
    • model キーワードでテーブルを定義
    • @id で主キーを指定
    • @default でデフォルト値を設定
    • @relation で関連を定義

    リレーションの種類:
    • 1 対 1:@relation(fields, references)
    • 1 対多:一方のモデルに @relation、もう一方には不要な場合あり
    • 多対多:中間テーブル(@relation テーブル)を使用

    マイグレーション生成:
    • npx prisma migrate dev --name init
  4. 4

    ステップ4: CRUD 操作の実装

    データ作成:
    • prisma.user.create({ data: { name, email } })

    データ取得:
    • prisma.user.findMany():複数件
    • prisma.user.findUnique({ where: { id } }):1 件
    • prisma.user.findFirst({ where: { ... } }):最初の 1 件

    データ更新:
    • prisma.user.update({ where: { id }, data: { name } })

    データ削除:
    • prisma.user.delete({ where: { id } })
  5. 5

    ステップ5: 関連クエリの処理

    include の使用:
    • prisma.user.findMany({ include: { posts: true } })
    • ユーザーとその全記事を返す

    select の使用:
    • prisma.user.findMany({ select: { id: true, name: true } })
    • 指定フィールドのみ返し、取得データを削減

    N+1 問題の回避:
    • include で関連データを一度に取得
    • 外側のループ内で関連を都度クエリしない
  6. 6

    ステップ6: デプロイとマイグレーション

    本番デプロイ:
    • DATABASE_URL 環境変数を設定
    • npx prisma generate でクライアント生成
    • npx prisma migrate deploy でマイグレーション適用

    Vercel デプロイ:
    • Vercel Dashboard で環境変数を設定
    • build コマンドに prisma generate を追加
    • prisma migrate deploy でマイグレーション適用

    注意:本番環境で migrate dev は実行しない

FAQ

なぜ 'too many clients already' エラーが出るのですか?
Next.js のホットリロードによる接続リークが原因です。

コードを変更するたびに Next.js が新しい PrismaClient インスタンスを作りますが、古い接続は自動で閉じられず、最終的に接続プールが枯渇します。

対策はシングルトンパターンで、開発環境では globalThis に PrismaClient インスタンスをキャッシュすることです。
Prisma と TypeORM、Drizzle の違いは?
Prisma:
• 型安全性が最も高く、開発体験が良い
• ただしパフォーマンスに一定のオーバーヘッドあり

TypeORM:
• 機能が豊富で複雑なクエリに対応
• 設定が複雑になりがち

Drizzle:
• 軽量でパフォーマンスが良い
• 型安全性は Prisma ほどではない

Next.js プロジェクトでは、Prisma の使いやすさと型安全のメリットが大きいです。
1 対多のリレーションはどう設計しますか?
「多」側のモデルに外部キーフィールドを追加し、@relation でマークします。

例:User が複数の Post を持つ場合
• Post モデルに userId と @relation(fields: ['userId'], references: [id]) を追加
• User モデルに posts Post[] フィールドを追加
include と select の違いは?
include:
• 関連クエリ用。関連データを返す
• 取得データ量が増える

select:
• フィールド選択用。指定フィールドのみ返す
• 取得データ量が減る

併用も可能:{ include: { posts: true }, select: { id: true, name: true } }
N+1 クエリ問題をどう避けますか?
外側のループで都度クエリするのではなく、include で関連データを一度に取得します。例:prisma.user.findMany({ include: { posts: true } }) は全ユーザーとその記事を一度に取得し、ユーザーごとに記事を別クエリしません。
Prisma はトランザクションに対応していますか?
対応しています。prisma.$transaction([...]) で複数操作を実行し、すべて成功するかすべて失敗します。例:await prisma.$transaction([prisma.user.create(...), prisma.post.create(...)])。
Vercel に Prisma をデプロイするには?
手順:
1) Vercel Dashboard で DATABASE_URL 環境変数を設定
2) package.json の build コマンドに prisma generate を追加
3) prisma migrate deploy でマイグレーション適用(migrate dev は使わない)

本番 DB 接続が正常であることを確認してください。

7分で読めます · 公開日: 2025年12月20日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます