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

Next.js + Prisma 完全ガイド:DB接続の落とし穴から本番運用まで

「SQL を書きたくない、でも NoSQL だとリレーションが辛い」
そんなフロントエンド出身のフルスタックエンジニアにとって、Prisma は救世主のような存在です。型安全性、直感的な API、そして見やすいスキーマ定義ファイル。一度使ったら、もう生の SQL 文字列を組み立てる作業には戻れません。

しかし、Next.js と Prisma を組み合わせる際には、いくつか落とし穴があります。
最も有名なのが、開発中に「Too many connections(接続数が多すぎます)」というエラーが出てデータベースが死ぬ現象です。これは Next.js のホットリロードの仕組みと関係しています。

この記事では、Prisma の導入から、この悪名高い接続問題の解決、リレーションの扱い、そしてパフォーマンスを意識したクエリの書き方まで、実務で必要な知識を一本にまとめました。

なぜ Prisma なのか?

Prisma は「次世代の ORM(Object-Relational Mapping)」と呼ばれています。従来の ORM との最大の違いは、型生成エンジンです。

schema.prisma というファイルを一つ書くだけで、そこからデータベースのマイグレーション用 SQL と、TypeScript の型定義ファイルの両方を生成してくれます。これにより、「DB のカラム名を変えたのに、コードの型定義を直し忘れて実行時エラー」という事故が物理的に起こらなくなります。

ステップ 1: セットアップとデータベース接続

まず、Next.js プロジェクトに Prisma をインストールします。

npm install prisma --save-dev
npm install @prisma/client
npx prisma init

これで prisma フォルダと .env ファイルが生成されます。
.envDATABASE_URL を自分のデータベース(PostgreSQL, MySQL など)に合わせて書き換えてください。

【重要】開発環境での接続リークを防ぐ

ここがこの記事で一番重要なポイントです。
何も考えずに const prisma = new PrismaClient() を書くと、Next.js でファイルを保存(ホットリロード)するたびに新しい接続プールが作られ、すぐに DB の最大接続数に達してエラーになります。

これを防ぐために、シングルトンインスタンスを作成するヘルパーファイルを作ります。

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

// global オブジェクトに prisma プロパティを追加して型拡張
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

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

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

今後は、アプリ内のどこでもこの lib/prisma.ts から prisma をインポートして使ってください。これで開発中も接続数は1つに保たれます。

ステップ 2: スキーマ設計とリレーション

ブログシステムを例に、prisma/schema.prisma を定義してみましょう。

// 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?
  posts     Post[]   // 1対多の関係(一人のユーザーは複数の記事を持つ)
  profile   Profile? // 1対1の関係
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id]) // 外部キー
  tags      Tag[]    // 多対多の関係(暗黙の中間テーブルが作られる)
  createdAt DateTime @default(now())
}

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

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

書き終わったら、マイグレーションを実行して DB に反映させます。

npx prisma migrate dev --name init

これで DB にテーブルが作成され、同時に @prisma/client の型定義も更新されます。

ステップ 3: CRUD 操作の実践

Prisma のクエリは直感的です。lib/prisma.ts をインポートして使います。

データの作成(Create)

リレーションがあるデータもまとめて作れます(ネストされた書き込み)。

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    posts: {
      create: [
        { title: '初めてのブログ', content: 'こんにちは!' },
        { title: 'Prismaの使い心地', published: true },
      ],
    },
  },
})

データの読み取り(Read)

include を使うと、関連するテーブルのデータも一度に取得できます(JOIN)。

// 全記事を取得し、著者の情報も含める
const posts = await prisma.post.findMany({
  where: {
    published: true,
  },
  include: {
    author: {
      select: { name: true }, // authorから名前だけを取得
    },
    tags: true,
  },
})

select を使うと、特定のフィールドだけを取得して転送量を減らせます。includeselect は同じ階層では併用できないので注意が必要です(ネストすればOK)。

データの更新(Update)

const updatedPost = await prisma.post.update({
  where: { id: 1 },
  data: {
    published: true,
    viewCount: {
      increment: 1, // 現在の値 + 1
    },
  },
})

トランザクション処理

複数の処理を「全部成功するか、全部失敗するか」にしたい場合は $transaction を使います。

const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]' } }),
  prisma.post.create({ data: { title: 'Bobの記事' } }), // authorId不足で失敗すると、上のuser作成もロールバックされる
])

パフォーマンス・ベストプラクティス

1. N+1 問題を避ける

やってはいけない例:

const users = await prisma.user.findMany();
for (const user of users) {
  // ループの中でクエリを投げている(最悪!)
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

正しい例:

const users = await prisma.user.findMany({
  include: { posts: true }, // 一回のクエリで取得
});

2. インデックスを貼る

検索によく使うカラムにはインデックスを貼りましょう。

model User {
  // ...
  email String @unique

  @@index([email]) // インデックス追加
}

3. 不要なデータは取得しない

findMany() はデフォルトですべてのカラムを取得します(SELECT *)。巨大なテキストデータなどがある場合は、select で必要なカラムだけを指定しましょう。

まとめ

Prisma は Next.js 開発におけるデータ層の最良のパートナーです。

  • 型安全性: 入力補完が効き、typo を撲滅できる。
  • 生産性: リレーションやマイグレーションが直感的。
  • 安心感: シングルトンパターンで接続リークも怖くない。

最初は少し独自の記法(schema.prisma)を覚える必要がありますが、その投資効果は絶大です。

Next.js + Prisma 導入フロー

インストールから接続設定、開発環境での接続リーク対策までの手順

⏱️ Estimated time: 15 min

  1. 1

    Step1: インストールと初期化

    npm install prisma @prisma/client を実行し、npx prisma init で設定ファイルを生成します。
  2. 2

    Step2: 環境変数の設定

    .env ファイルの DATABASE_URL を実際のデータベース接続文字列に変更します。
  3. 3

    Step3: Prisma クライアントのシングルトン化

    lib/prisma.ts を作成し、globalThis を使って開発環境でクライアントインスタンスを再利用するコードを記述します。これが接続リーク対策になります。
  4. 4

    Step4: スキーマの定義

    prisma/schema.prisma にモデル(テーブル)定義を記述します。
  5. 5

    Step5: マイグレーション実行

    npx prisma migrate dev --name init を実行し、DB にテーブルを作成して型定義を生成します。

FAQ

開発中に 'Too many connections' エラーが出ます。
Next.js のホットリロードにより、ファイル変更のたびに新しい PrismaClient インスタンスが作成されてしまうのが原因です。`globalThis` を使用してインスタンスをキャッシュするシングルトンパターン(記事中の `lib/prisma.ts`)を実装してください。
Prisma は本番環境でも使えますか?パフォーマンスは?
はい、多くの大規模サービスで採用されています。ただし、複雑な集計クエリなどは苦手な場合があるため、その場合は `prisma.$queryRaw` で生の SQL を書くこともできます。一般的な Web アプリの CRUD 操作なら十分高速です。
マイグレーションファイルは Git に含めるべきですか?
はい、`prisma/migrations` フォルダはバージョン管理に含めるべきです。これにより、チームメンバーや本番環境で同じ DB 構造を再現(`prisma migrate deploy`)できます。
Vercel などのサーバーレス環境での注意点は?
サーバーレス関数は頻繁に起動・終了するため、DB への接続プールが枯渇しやすいです。Prisma Data Proxy や Vercel Postgres、Supabase などのプール機能を持つサービスと組み合わせるか、接続プールサイズ(connection_limit)を適切に制限する必要があります。

3 min read · 公開日: 2025年12月20日 · 更新日: 2026年1月22日

コメント

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

関連記事