「データアクセスを型安全にしたい」——要件としては一行です。けれど本番へ載せようとした瞬間、判断すべきことが一気に増えます。スキーマをどこで宣言するのか。型はどこから来るのか。リレーションでN+1を出さないには。トランザクション境界はどこに引くのか。マイグレーションをどう生成・レビュー・適用するのか。SQLインジェクションをどう構造的に防ぐのか。サーバーレスでコネクションをどう枯渇させないのか。
この記事は、Prisma ORM を v7 世代で本番品質に運用するための実装ガイドです。Prisma は「スキーマを唯一の真実源(single source of truth)にして、そこから型・クライアント・マイグレーションをすべて導出する」という思想のORMで、宣言的なデータモデルと開発体験の良さで広く使われています。そして 2025年11月の v7 で、内部アーキテクチャが大きく変わりました——長らく同梱されていた Rust製クエリエンジンが廃止され、driver adapter が必須になり、クライアントの生成方式(generator)も新しくなっています。本記事はこの v7 を前提に、公式ドキュメントに忠実な事実を土台として、私が普段から徹底している「実行時の正しさを、できる限り型に押し込む」設計思想を重ねて解説します。
この記事のルール:API仕様・コマンド・コードは Prisma 公式ドキュメント(2026年6月時点、v7系) に基づきます。Prisma は活発に進化しており、generator・adapter・CLI フラグは版で変わります(本記事の
prisma-clientgenerator・driver adapter 必須化・prisma.config.tsはいずれも v7 で既定になった挙動です)。本番投入前に必ず公式ドキュメントで最新仕様を確認してください。DB接続情報(接続文字列・認証情報)は環境変数前提で扱います(ハードコード厳禁)。一部に「Preview(プレビュー)」段階の機能が含まれる箇所は明記します。
0. メンタルモデル:ORM境界も「型」で固める
本題の前に、この記事を貫く一本の軸を共有させてください。
私は型安全を徹底したサブスク課金基盤の開発で、as / any / enum / non-null assertion を全面禁止しました。代わりに Union 型+satisfies+NeverError(value: never) を組み合わせ、「分岐の取りこぼしがあればコンパイルが落ちる」状態を作り、外部入力の境界はすべて Zod で検証しました。
この思想——「ランタイムの不正を、できるだけコンパイル時に倒す」——は、ORM境界(DBアクセス)にもそのまま当てはまります。むしろデータベースは、アプリの中で最も型が崩れやすい境界です。SELECT * の結果が any のまま流れてくれば、その先のロジックはいくら丁寧に書いても砂上の楼閣になります。
Prisma のメンタルモデルはこうです。
schema.prismaでデータモデルを宣言する →prisma generateでスキーマに対応する型付きクライアントが生成される → クエリの入力も結果も型が通る = スキーマが唯一の真実源。
スキーマが single source of truth になり、そこから型もクライアントもマイグレーションも導出される。これが Prisma の中核であり、私の設計原則とまっすぐ噛み合う理由です。Prisma と Drizzle の最大の違いは「スキーマを独自DSL(.prisma)で書くか、TypeScript で書くか」という点に集約されますが(後述の §13 で使い分けを整理します)、「スキーマから型を機械生成する」という規律はどちらも共通です。
1. v7 で何が変わったのか(最初に押さえる)
Prisma を以前から使っている人ほど、まずここを読んでください。v7 はセットアップが変わったため、古い記事のコードがそのままでは動きません。
| 領域 | v6(旧) | v7(現行) |
|---|---|---|
| generator | prisma-client-js(node_modules に生成) | prisma-client(output で生成先を明示) |
| クエリエンジン | Rust製バイナリを同梱 | Rustフリー(TypeScript/WASM のクエリコンパイラ) |
| driver adapter | 任意(Preview) | 全DBで必須(PostgreSQLは @prisma/adapter-pg) |
ミドルウェア $use | あり | 削除 → Client Extensions($extends)へ |
Prisma.validator | あり | レガシー扱い → TypeScript の satisfies へ |
| 設定の置き場所 | package.json の "prisma" キー | prisma.config.ts(defineConfig) |
| import 元 | @prisma/client | 生成先パス(例:../generated/prisma/client) |
Rustフリー化は単なる内部刷新ではなく、運用に効く実利があります。公式ブログによれば、新アーキテクチャはクエリが最大 3.4 倍高速になり、**バンドルサイズが約 14MB → 1.6MB(約90%減)**まで縮みました。ネイティブバイナリ依存が消えることで、サーバーレス/エッジへのデプロイが大幅に単純化されます——コールドスタートとアーティファクトサイズが効く環境ほど恩恵が大きい変更です。
設計判断:新規プロジェクトは迷わず v7(
prisma-clientgenerator)で始めるべきです。既存の v6 プロジェクトは、公式のv7アップグレードガイドに沿って、generator・adapter・import パスを移行します(§14 で要点をまとめます)。
2. セットアップ:generator・driver adapter・prisma.config.ts
2.1 インストール
PostgreSQL を題材にします。クエリエンジンが無くなった代わりに、接続を担う driver adapter を明示的に入れます。
npm install @prisma/client @prisma/adapter-pg
npm install -D prisma tsx
2.2 スキーマと generator
prisma/schema.prisma の冒頭で、データソースと 新しい generator を宣言します。v7 では provider = "prisma-client"、そして output(生成先)が必須です。
// prisma/schema.prisma
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client" // v7の新generator(旧: prisma-client-js)
output = "../src/generated/prisma" // 生成先を明示(node_modulesには出ない)
runtime = "nodejs" // deno / bun / workerd / vercel-edge なども選べる
moduleFormat = "esm" // esm | cjs
}
ここが v7 の最大の変化点です。生成されたクライアントは node_modules ではなく、あなたのリポジトリ内(output のパス)に出力されます。つまり生成物が「自分のコードの一部」として可視化され、import 元も @prisma/client ではなく生成先パスになります。runtime を切り替えるだけで Bun・Deno・Cloudflare Workers・Vercel Edge 向けのクライアントを生成できるのも、Rustフリー化(=ネイティブバイナリ非依存)の果実です。
運用のコツ:
outputで指定したディレクトリ(例src/generated/)は生成物なので.gitignoreに入れ、CI とデプロイのビルド前に必ずprisma generateを走らせます(postinstallか明示的なビルドステップで)。生成物をコミットするか否かはチーム方針ですが、少なくとも「生成し忘れたまま本番に出る」事故をビルド手順で塞ぐことが重要です。
2.3 prisma.config.ts(v7の設定起点)
v7 では、CLI がスキーマ・マイグレーション・シードをどう扱うかを prisma.config.ts(プロジェクト直下)で型安全に宣言します。環境変数は自動では読み込まれないため、dotenv 等で明示的に読み込みます。
// prisma.config.ts(プロジェクト直下)
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts", // シードは migrations.seed 配下
},
datasource: {
url: env("DATABASE_URL"),
},
});
設定がコードになることで、モノレポでもデプロイでも再現性が担保され、package.json の文字列キーに依存していた頃の曖昧さが消えます。
2.4 PrismaClient の生成(adapter 必須)
v7 では、クライアント生成時に driver adapter を渡すことが必須です。これがアプリ全体で使い回す「DBハンドル」になります。
// src/db.ts
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client"; // ← 生成先からimport
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
export const prisma = new PrismaClient({ adapter });
PrismaClient を @prisma/client からではなく生成先 ./generated/prisma/client から import している点に注目してください。v7 を語る記事はここでつまずきがちです。接続プール等のチューニングは、接続文字列のクエリパラメータではなくadapter 側のオプションで行う方向に整理されました(プールの既定値も v7 で見直されています)。
3. スキーマ・アズ・コード:model で宣言する
Prisma のスキーマは、model ブロックで「データの形」を宣言します。これが型・クライアント・マイグレーションすべての源泉です。
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
createdAt DateTime @default(now())
@@index([authorId, title])
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id])
postId Int
@@map("comments") // 物理テーブル名を comments に
}
enum Role {
USER
ADMIN
}
読み解きの要点を、設計の観点から押さえます。
@id @default(autoincrement())は主キーと自動採番。@uniqueは一意制約。String?の?は NULL 許容(=name: string | null)を意味します。- リレーションは2つで1組です。
Post側のauthor User @relation(fields: [authorId], references: [id])が外部キー側、User側のposts Post[]が逆参照。Profile.userIdに@uniqueを付けることで「1ユーザーにつき1プロフィール」という1対1が型レベルで表現されます。 @@index([authorId, title])は複合インデックス、@@map("comments")はモデル名(コード上)と物理テーブル名(DB上)を分離します。コードはComment、DBはcomments——この分離は既存DBへの後付け導入で効きます。enumは DB の列挙型に対応し、Role.USERのように型安全に扱えます(私は手書きの文字列リテラル enum を避け、こうしてスキーマ駆動にします)。
設計のコツ(SRP):スキーマは「データの形」という1つの責務に徹します。集計用のビューやアプリ固有の派生値をここに混ぜず、ビジネスロジックはアプリ層へ。スキーマが肥大化したら、Prisma は複数ファイルへの分割もサポートします。
スキーマを書き換えたら npx prisma generate で型付きクライアントを再生成します(後述のマイグレーションコマンドは生成も兼ねます)。
4. 型安全:結果型は「推論させる」、名付けない
ここが Prisma の真価です。スキーマから、入力型も結果型も機械生成されます。手書きの DTO 型や as キャストでこの境界をまたぐのは、型安全を自ら捨てる行為です。
import { Prisma } from "./generated/prisma/client";
// 1) 入力型:スキーマの制約(idは自動採番なので不要、nameは任意)が型に反映される
const data: Prisma.UserCreateInput = {
email: "hinata@example.com",
posts: { create: { title: "Hello Prisma v7" } },
};
// 2) 結果型:include/select の「形」から正確な戻り型を導出する
const userWithPosts = { include: { posts: true } } satisfies Prisma.UserDefaultArgs;
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>;
// UserWithPosts は { id; email; name: string | null; ...; posts: Post[] } に正確に推論される
ポイントは2つです。
Prisma.UserCreateInputのような生成型を使えば、「INSERT 時に何を渡せて何を渡せないか」がスキーマと完全に一致します。idはautoincrement()なので渡せない、nameは?なので省略できる——この差分を人間が同期し続ける必要がありません(DRY 違反の典型を構造的に排除)。Prisma.UserGetPayload<typeof args>は、include/selectで指定した「取得する形」から正確な結果型を導出します。postsを含めたなら結果型にもpostsが現れ、含めなければ現れない。これをsatisfies(TypeScript ネイティブ演算子)と組み合わせるのが v7 推奨の作法です。
v7 の注意:旧来の
Prisma.validator<...>()は**レガシー(prisma-client-js専用)**で、新しいprisma-clientgenerator では使えません。代わりに上記のsatisfies+GetPayloadを使ってください。「境界の型は推論させ、自分で名付けない」——これが Prisma を使ううえでの第一の規律です。
5. CRUD:基本操作を一望する
prisma.<model>.<操作> という統一されたAPIです。where / select / orderBy / take / skip など、引数の名前と意味がモデル横断で一貫しているのが Prisma の学習効率の高さです。
// create
await prisma.user.create({
data: { email: "elsa@prisma.io", name: "Elsa" },
});
// createMany(重複は skipDuplicates で握りつぶせる)
await prisma.user.createMany({
data: [
{ email: "bob@prisma.io", name: "Bob" },
{ email: "yewande@prisma.io", name: "Yewande" },
],
skipDuplicates: true,
});
// findUnique(一意キーでの単一取得)
const user = await prisma.user.findUnique({ where: { email: "elsa@prisma.io" } });
// findMany(条件・並び・ページング・必要な列だけ)
const users = await prisma.user.findMany({
where: { email: { endsWith: "@prisma.io" } },
select: { id: true, email: true }, // 取りすぎ防止(コスト&機密の最小化)
orderBy: { id: "asc" },
take: 10,
skip: 20,
});
// update / upsert / delete
await prisma.user.update({
where: { email: "elsa@prisma.io" },
data: { name: "Elsa the Great" },
});
await prisma.user.upsert({
where: { email: "viola@prisma.io" },
update: { name: "Viola v2" },
create: { email: "viola@prisma.io", name: "Viola" },
});
await prisma.user.delete({ where: { email: "bob@prisma.io" } });
実務上のコツを3つ。
selectで取る列を絞るのは、パフォーマンスと機密の最小化の両面で効きます。password_hashのような列を「selectしなければ漏れない」状態に保つのは、安全側に倒れる良い既定です。upsertはwhere(探す)・update(在れば)・create(無ければ)の3点セット。これは後述の冪等性設計の主役になります。- ページングは
take/skip(オフセット)とcursor(カーソル)の2方式。大きなテーブルではオフセットは深いページで劣化するため、cursor: { id: lastId }+orderByのカーソル方式を選びます。
6. リレーションと N+1:include / select で「1回で」取る
ORM で最も事故が多いのが N+1 問題(一覧を取ってから1件ずつ関連を引き、クエリが行数分に膨れる)です。Prisma では関連を include(丸ごと)/ select(選んで) で宣言的に取得し、行ごとに追加クエリを発行しないため、素直に書けば N+1 が出ません。
// include:関連レコードを丸ごと
const usersWithPosts = await prisma.user.findMany({
include: { posts: true },
});
// select:ネストして、必要な列だけ
const slim = await prisma.user.findMany({
select: {
name: true,
posts: { select: { title: true } },
},
});
ネストした書き込みも強力です。親と子を1トランザクションで作れます(明示的な $transaction 不要——公式は「ネスト書き込みは単一トランザクションで複数操作を行う」と定義しています)。
// 親ユーザーと記事を同時に作成
await prisma.user.create({
data: {
email: "vlad@prisma.io",
posts: { create: [{ title: "First" }, { title: "Second" }] },
},
});
// 既存を connect、無ければ作る connectOrCreate
await prisma.post.create({
data: {
title: "How to make croissants",
author: {
connectOrCreate: {
where: { email: "viola@prisma.io" },
create: { email: "viola@prisma.io", name: "Viola" },
},
},
},
});
発展(Preview 機能):取得時の関連ロード戦略を選ぶ
relationLoadStrategy: "join" | "query"があります。"join"は DB 側のLATERAL JOIN(PostgreSQL)等で単一クエリ化し、"query"は複数クエリをアプリ側で結合します。本機能は執筆時点で Preview のため、本番採用は機能フラグと検証を前提にしてください。いずれにせよ、include/selectは行数に比例してクエリが増えないので、まず素直に書けば N+1 は回避できます。
7. トランザクションと冪等性:境界をコードで固める
決済や在庫のような「正しさ」が要る処理は、運用の注意深さではなくコードの構造で守ります。Prisma のトランザクションは2系統です。
7.1 配列形(独立した複数クエリをまとめてアトミックに)
const [posts, total] = await prisma.$transaction([
prisma.post.findMany({ where: { title: { contains: "prisma" } } }),
prisma.post.count(),
]);
7.2 対話形(途中の値で分岐する一連の処理)
import { Prisma } from "./generated/prisma/client";
await prisma.$transaction(
async (tx) => {
const sender = await tx.account.update({
where: { email: "alice@prisma.io" },
data: { balance: { decrement: 100 } },
});
if (sender.balance < 0) throw new Error("Insufficient funds"); // throwで全ロールバック
await tx.account.update({
where: { email: "bob@prisma.io" },
data: { balance: { increment: 100 } },
});
},
{
maxWait: 5_000, // トランザクション開始を待つ上限
timeout: 10_000, // トランザクション全体の上限
isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // 競合に強い分離レベル
},
);
実務で外せない規律を3つ。
- トランザクション内では必ず
txを使う。txではなく外側のprismaを呼ぶと、その操作はトランザクションの外で走り、原子性が壊れます。 - 外部API呼び出し(決済プロバイダ等)はトランザクション境界の外へ。ネットワークI/Oをトランザクション内に抱えると、DBの接続とロックを長時間占有し、タイムアウトとデッドロックの温床になります。
- 冪等性は
upsertと一意制約で作る。リトライや重複配信を前提に、@uniqueな冪等キー(idempotencyKey等)でupsertすれば、「同じ操作が2回来ても結果が1回分」になります。重複時のP2002(一意制約違反)を捕まえて握りつぶすのも常套手段です(§9)。
関連:冪等性・二重課金対策の設計思想は、決済を題材にした別記事で深掘りしています。本番二重課金0件を支えた考え方は、ORM が Prisma でも Drizzle でも、また DynamoDB でも普遍です。
8. 生SQLとセキュリティ:必ずパラメータ化する
ORM で表現しづらい集計やDB固有機能には生SQLを使いますが、ここがSQLインジェクションの最大の入口です。Prisma の安全な書き方はタグ付きテンプレート($queryRaw / $executeRaw)です。${} は文字列連結ではなく**バインドパラメータ(プリペアドステートメント)**になり、注入が構造的に起きません。
// ✅ 安全:${email} はパラメータとして束縛される
const email = "emelie@prisma.io";
const rows = await prisma.$queryRaw`SELECT id, name FROM "User" WHERE email = ${email}`;
// ✅ IN 句は Prisma.join で安全に展開
import { Prisma } from "./generated/prisma/client";
const ids = [1, 3, 5, 10];
await prisma.$queryRaw`SELECT * FROM "User" WHERE id IN (${Prisma.join(ids)})`;
// ✅ 条件付き断片は Prisma.sql / Prisma.empty で組み立てる
const name: string | null = null;
await prisma.$queryRaw`
SELECT * FROM "User"
${name ? Prisma.sql`WHERE name = ${name}` : Prisma.empty}
`;
// ✅ 件数を返す書き込み系は $executeRaw(影響行数を number で返す)
const affected: number = await prisma.$executeRaw`
UPDATE "User" SET active = ${true} WHERE "emailValidated" = ${true}
`;
逆に、やってはいけない形がこれです。
// ❌ 厳禁:文字列連結はインジェクションの穴。ユーザー入力を絶対に渡さない
const input = '"x" UNION SELECT id, title FROM "Post"';
await prisma.$queryRawUnsafe("SELECT id, name FROM \"User\" WHERE name = " + input);
$queryRawUnsafe と Prisma.raw(...) は「エスケープされない生の断片」を差し込むAPIで、ユーザー入力を渡せば即座に脆弱性になります。やむを得ず使う場合でも、入力が自分の管理下の固定値であることを保証し、外部入力は必ずパラメータ側(${} / Prisma.join)に置きます。なお Prisma は型安全に生SQLを書く TypedSQL も提供しており、生SQLが必要な場面ではまずこちらの採用を検討する価値があります。
9. エラーハンドリング:コードで分岐する
Prisma の既知エラーは Prisma.PrismaClientKnownRequestError として投げられ、code で原因を判別できます。v7 では Prisma 名前空間も生成先パスから import します。
import { Prisma } from "./generated/prisma/client";
try {
await prisma.user.create({ data: { email: "dup@example.com" } });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
// 一意制約違反:冪等な再試行なら 200/既存を返す、UI なら「既に登録済み」
return { ok: false, reason: "duplicate" as const };
}
if (e.code === "P2025") {
// 対象レコードが無い(update/delete の前提が崩れた)
return { ok: false, reason: "not_found" as const };
}
}
throw e; // 想定外は握りつぶさず再送出(可観測性のため)
}
代表的なコードは P2002(一意制約違反) と P2025(対象レコードが見つからない) です。これらを型付きの結果(Union)に翻訳し、呼び出し側で switch 漏れがあればコンパイルが落ちる——という設計にすると、エラー処理が「祈り」から「保証」に変わります。想定外のエラーまで握りつぶさないこと(再送出してログ/トレースに残す)も、可観測性の観点で重要です。
10. Client Extensions:横断的関心を型安全に足す(v7でミドルウェア廃止)
v7 でミドルウェア($use)は削除され、代わりに Client Extensions($extends) が正式な拡張点になりました。model / client / query / result の4種で、計算列の付与やクエリのロギングなどを型安全に差し込めます。
// query 拡張:全モデル・全操作の所要時間をログ(可観測性)
const prisma = basePrisma.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const start = performance.now();
const result = await query(args);
const ms = Math.round(performance.now() - start);
console.log(JSON.stringify({ event: "db_query", model, operation, ms }));
return result;
},
},
},
});
ミドルウェアと違い、Extensions は拡張ごとに新しいクライアントを返すため、適用範囲を限定でき、型も保たれます。ログは相関ID付きの構造化ログにして、トレースと突き合わせられる形にしておくのが本番運用の定石です。
11. マイグレーション:dev と deploy を正しく分ける
スキーマ変更を版管理し、安全に本番へ適用するのが Prisma Migrate です。コマンドの役割分担を間違えると本番でデータを失います——ここは強調します。
# 開発:差分から新しいマイグレーションを生成して即適用(+クライアント再生成)
npx prisma migrate dev --name add_user_role
# 本番/CI:保留中のマイグレーションだけを順に適用(生成もリセットもしない)
npx prisma migrate deploy
# 試作のみ:マイグレーション履歴を残さずスキーマをDBへ同期(使い捨て)
npx prisma db push
| コマンド | 用途 | 重要な性質 |
|---|---|---|
migrate dev | 開発専用 | 差分検出のためシャドウDBが必要。ドリフト検出時にDBをリセット(=データ破棄)し得る。本番で打ってはいけない |
migrate deploy | 本番/CI | 保留中のマイグレーションのみ適用。ドリフト検査もリセットもシャドウDBも使わないので安全 |
db push | 試作・スキーマ実験 | _prisma_migrations を作らず履歴も残さない。版管理が不要な使い捨て検証専用 |
シャドウDBは、migrate dev がスキーマのドリフトやデータ損失を検出するために自動で作成・削除する一時DBです。本番には不要(migrate deploy は使わない)ですが、開発環境/CIでマイグレーションを生成する際に接続権限が要る点に注意します。
本番フロー:開発で
migrate devしてマイグレーションファイルを生成 → 生成された SQL を必ず人間がレビュー(破壊的変更がないか)→ コミット → CI がmigrate deployで本番に適用。無停止スキーマ変更(ロックを避けるDDL、列追加→バックフィル→制約付与の段階適用)は、生成されたSQLをこのレビュー段で点検して担保します。
12. サーバーレス/Next.js:接続を枯渇させない
Prisma の本番事故で最も多いのが 接続枯渇です。サーバーレスは関数インスタンスが大量に立ち上がり、各々が DB 接続を握ると、PostgreSQL の max_connections をあっという間に食い潰します。Rustフリー化でアーティファクトは軽くなりましたが、接続管理の原則は変わりません。
12.1 開発のホットリロード対策(シングルトン)
Next.js の next dev は再読み込みのたびに PrismaClient を再生成しがちで、接続が増殖します。globalThis に保持して使い回します。
// src/db.ts
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client";
const createClient = () =>
new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) });
const globalForPrisma = globalThis as unknown as { prisma?: ReturnType<typeof createClient> };
export const prisma = globalForPrisma.prisma ?? createClient();
// 本番ではグローバルに保持しない(コールドスタートごとに1個で十分)
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
12.2 サーバーレス本番の原則
- クライアントはハンドラの外で生成して、ウォームなインスタンス間で再利用する。
$disconnect()を毎リクエストで呼ばない。接続の確立・破棄を繰り返すと逆にコストになります。- 外部プーラを噛ませる。多数の関数が直接 DB を叩く構成では、PgBouncer などのプーラ、あるいは後述の Prisma Accelerate / Prisma Postgres を前段に置きます。
12.3 Prisma Postgres / Accelerate(マネージドな接続プールとキャッシュ)
Prisma Postgres は、PgBouncer による接続プールを内蔵したマネージド PostgreSQL で、サーバーレス/エッジでの接続枯渇を構成側で解消してくれます(プールやエッジ対応は通常の接続文字列で自動的に効きます)。Prisma Accelerate は、外部DBにも使えるグローバルな接続プール+クエリキャッシュのレイヤで、@prisma/extension-accelerate の withAccelerate() を $extends し、クエリ単位で cacheStrategy を指定できます。
import { withAccelerate } from "@prisma/extension-accelerate";
const prisma = basePrisma.$extends(withAccelerate());
// クエリ単位でキャッシュ(ttl=鮮度、swr=stale許容秒)
const posts = await prisma.post.findMany({
where: { published: true },
cacheStrategy: { ttl: 60, swr: 10 },
});
注意:Accelerate の正確な接続文字列・初期化(特に v7 の driver adapter との併用)は版で細部が動くため、本番投入前に必ず公式ドキュメントで最新の手順を確認してください。
cacheStrategyはキャッシュ可能な読み取りにのみ使い、整合性が要る読み取りには付けない、という線引きが重要です。
13. Prisma か Drizzle か:使い分けの軸
「型安全な TypeScript ORM」という土俵で必ず比較されるのが Drizzle です。私は両方を本番で使いますが、選定軸はシンプルです。
| 観点 | Prisma | Drizzle |
|---|---|---|
| スキーマの書き方 | 独自DSL(schema.prisma)。宣言的で読みやすい | TypeScript(pgTable)。学習対象が増えない |
| 抽象度 | 高め。リレーション取得が宣言的で楽 | 低め。SQLに近く透明性が高い |
| 型の源泉 | スキーマ → prisma generate で生成 | スキーマ(TS)→ $inferSelect で推論 |
| マイグレーション | Prisma Migrate(成熟・運用機能が厚い) | drizzle-kit(生成→適用がシンプル) |
| エッジ/軽量性 | v7 でRustフリー化し大幅改善 | もとから軽量・依存が薄い |
| マネージド連携 | Prisma Postgres / Accelerate が強い | アダプタで各社サーバーレスDBに対応 |
ざっくりの指針:
- 宣言的なデータモデルと厚いマイグレーション運用、マネージドな接続プール/キャッシュ込みで素早く本番化したい → Prisma。
- SQL の透明性を保ちたい・依存を最小にしたい・生成物より推論で完結させたい → Drizzle。
どちらも「スキーマを唯一の真実源にして型を機械生成する」点は同じで、as/any で境界をまたがない規律さえ守れば、本番品質に到達できます。プロジェクトの制約(既存DB・チームのSQL習熟度・デプロイ先)で選べばよく、宗教戦争にする必要はありません。
14. v6 → v7 移行の要点
既存プロジェクトを v7 へ上げる際の最小チェックリストです(詳細は公式のv7アップグレードガイド)。
- generator を
prisma-clientに変更し、outputを必須指定する(node_modules生成からの脱却)。 - driver adapter を導入(PostgreSQLは
@prisma/adapter-pg)し、new PrismaClient({ adapter })に書き換える。 - import 元を生成先パスに変更(
@prisma/client→./generated/prisma/client)。Prisma名前空間・エラー型も同様。 $useミドルウェアを Client Extensions($extends)へ移植する。Prisma.validatorをsatisfies+GetPayloadへ置換する。- 設定を
prisma.config.ts(defineConfig)へ移す。--skip-generate/--skip-seed等の一部CLIフラグは廃止された点も確認する。 - CI/デプロイのビルド前に
prisma generateが走ることを保証し、migrate deployで本番適用する。
段階移行の鉄則は「まず動く形に直してから、最適化する」です。output・adapter・import の3点を直せばまず動き、Extensions 化や Accelerate 導入は後追いで構いません。
15. 本番投入チェックリスト
最後に、Prisma を本番へ載せる前に私が必ず確認する項目です。
- generator は
prisma-client、outputを明示し、生成物は.gitignore+ ビルドでprisma generateを強制 -
PrismaClientは driver adapter 付きで生成し、アプリ全体で1インスタンス(サーバーレスはハンドラ外) - 接続文字列・認証情報は環境変数のみ(ハードコード・ログ出力なし)
- 結果型は
GetPayload/satisfiesで推論。as/anyでDB境界をまたがない - 一覧×関連は
include/selectで取得し、N+1 が出ていないかをクエリログで確認 - 「正しさ」が要る処理は
$transaction(対話形はtxのみ使用、外部I/Oは境界外、必要ならisolationLevel) - 冪等性は
@uniqueキー+upsert、P2002/P2025を型付き結果に翻訳 - 生SQLは
$queryRawのタグ付きテンプレートで必ずパラメータ化。$queryRawUnsafe/Prisma.rawにユーザー入力を渡さない - マイグレーションは開発
migrate dev→ SQLレビュー → CImigrate deploy。db pushは本番で使わない - サーバーレスは接続枯渇対策(プーラ/Prisma Postgres/Accelerate)と
cacheStrategyの整合性線引き - クエリ所要時間を Client Extensions で構造化ログ化し、トレースと相関
まとめ
Prisma v7 は、Rustエンジンの廃止と driver adapter の必須化によって「軽くて速く、サーバーレス/エッジに素直に乗る」ORM へと一段進化しました。セットアップは変わりましたが、本質は一貫しています——スキーマを唯一の真実源にし、型・クライアント・マイグレーションをそこから導出し、境界を as/any でまたがず、正しさをコードの構造(トランザクション・冪等性・パラメータ化)で保証する。この規律さえ守れば、Prisma は「速く書けて、本番で壊れない」データアクセス層になります。
「型安全なデータ層を、本番品質で素早く立ち上げたい」——もしそんな要件をお持ちなら、Prisma/Drizzle の選定からマイグレーション運用・サーバーレスの接続設計まで、実装に落とし込むお手伝いができます。