「Prisma の schema・migration・relation・transaction を実コードで一気に把握したい」「Drizzle / TypeORM / Kysely とどう違うのか比較したい」「Next.js や Edge Runtime で安全に使う方法を知りたい」。そんな声に応える完全実践ガイドです。本記事は Prisma 5.x + Node.js 22 LTS + TypeScript を前提に、prisma init から本番運用までを 40 個以上のコピペで動くコードで解説します。Drizzle / Kysely との比較、Next.js / Edge での Singleton と Accelerate、Pulse によるリアルタイムクエリ、testcontainers による統合テストも網羅します。
- Prisma 5.x の
schema.prismaから CRUD・transaction・raw SQL までの全コード - 1 対 1 / 1 対多 / 多対多のリレーション設計と nested write の書き方
- Next.js (App Router) と Edge Runtime での安全な使い方(Singleton + Accelerate)
- Drizzle / TypeORM / Kysely との明確な比較と選定基準
- testcontainers / Vitest による本物の Postgres を使った統合テスト戦略
- Connection Pooling・PgBouncer・pgvector など本番運用ノウハウ
- 1. Prisma の概要と Node.js 22 環境セットアップ
- 2. schema.prisma 完全解剖とリレーション設計
- 3. Prisma Migrate でスキーマを DB に反映する
- 4. Prisma Client で CRUD・検索・ページネーション
- 4.1 Client のインスタンス化
- 4.2 create / createMany
- 4.3 findUnique / findFirst / findMany
- 4.4 update / updateMany / upsert
- 4.5 delete / deleteMany
- 4.6 複雑な where 条件(AND / OR / NOT)
- 4.7 select で必要なカラムだけ取得
- 4.8 include で関連データを JOIN
- 4.9 orderBy / take / skip(オフセット pagination)
- 4.10 cursor pagination(大規模データで推奨)
- 4.11 nested write で作成と同時に子レコードを作る
- 4.12 既存レコードを connect / connectOrCreate
- 5. transaction と raw SQL
- 6. Client Extensions・logging・seed・Studio
- 7. Next.js / Edge Runtime / Pulse でフルスタック運用
- 8. テスト戦略(testcontainers + Vitest)
- 9. Drizzle / TypeORM / Kysely との比較とパフォーマンス・本番運用
- 10. まとめ・関連サービス・キャリア
1. Prisma の概要と Node.js 22 環境セットアップ
1.1 Prisma とは何か(3 つのコンポーネント)
Prisma は TypeScript / Node.js 向けの 次世代 ORM です。中身は実質 3 つのコンポーネントから成ります。Prisma Schema(schema.prisma)でデータモデルを宣言し、Prisma Migrate で DB に反映、Prisma Client が完全に型付けされたクエリ API を生成します。SQL を直接書かずに型安全な CRUD を実装でき、IDE 補完が極めて強力なのが最大の魅力です。
1.2 Prisma 5 系で押さえるべき変更点
Prisma 5 は Node.js 16.13 以上を必須とし、Rust エンジンの最適化、jsonProtocol のデフォルト化(クエリ実行が約 4〜10 倍高速化)、TypeScript 4.7 以上の必須化、fullTextSearch プレビューの安定化、Driver Adapters(neon, planetscale, libsql, pg)サポートが入りました。Edge Runtime / Workers でも動くようになっています。
1.3 Node.js / npm のバージョン確認
# Node.js 22 LTS を推奨(Prisma 5 は 16.13 以上で動作)
node -v
# v22.11.0
npm -v
# 10.9.0
1.4 プロジェクト初期化
mkdir prisma-app && cd prisma-app
npm init -y
npm i -D typescript @types/node tsx
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext
--esModuleInterop --strict --outDir dist
1.5 Prisma のインストール
# CLI(開発専用)と Client(ランタイム)を分けてインストール
npm i -D prisma
npm i @prisma/client
1.6 prisma init で初期化
# デフォルトは PostgreSQL
npx prisma init --datasource-provider postgresql
# SQLite で素早く始めたい場合
# npx prisma init --datasource-provider sqlite
実行すると prisma/schema.prisma と .env が生成されます。.env は必ず .gitignore へ。
echo ".env" >> .gitignore
echo "node_modules" >> .gitignore
2. schema.prisma 完全解剖とリレーション設計
2.1 最小の 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?
createdAt DateTime @default(now())
}
2.2 データソース別の書き方(4 種類)
// PostgreSQL
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // postgresql://user:pass@localhost:5432/dbname
}
// MySQL
datasource db {
provider = "mysql"
url = env("DATABASE_URL") // mysql://user:pass@localhost:3306/dbname
}
// SQLite(開発・テストに最適)
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
// MongoDB(リレーション制約はアプリ側で担保)
datasource db {
provider = "mongodb"
url = env("DATABASE_URL") // mongodb+srv://...
}
2.3 主要属性(@id / @unique / @default / @relation)
model Product {
id String @id @default(cuid()) // 衝突しない ID
sku String @unique // 一意制約
name String
priceYen Int @default(0)
isPublished Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // 更新時刻を自動更新
tags String[] @default([]) // Postgres の配列型
@@index([isPublished, createdAt]) // 複合インデックス
@@map("products") // テーブル名を明示
}
2.4 enum 定義
enum Role {
USER
EDITOR
ADMIN
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELED
}
model Account {
id Int @id @default(autoincrement())
role Role @default(USER)
}
2.5 ID 戦略の選び方
// 1) 自動採番 Int(最もシンプル・小規模向け)
model A { id Int @id @default(autoincrement()) }
// 2) cuid()(衝突しにくい短い文字列・推奨)
model B { id String @id @default(cuid()) }
// 3) uuid()(分散システム・URL に出してもよい用途)
model C { id String @id @default(uuid()) }
// 4) 複合主キー(中間テーブル等)
model UserOnTeam {
userId Int
teamId Int
@@id([userId, teamId])
}
2.6 1 対多(最も基本)
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[] // 仮想フィールド(リレーション側)
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int // 外部キーカラム
author User @relation(fields: [authorId], references: [id])
}
2.7 1 対 1(プロフィール拡張)
model User {
id Int @id @default(autoincrement())
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
userId Int @unique // 一意制約で 1 対 1 になる
user User @relation(fields: [userId], references: [id])
}
2.8 多対多(明示的な中間テーブル)
model Post {
id Int @id @default(autoincrement())
tags PostOnTag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts PostOnTag[]
}
model PostOnTag {
postId Int
tagId Int
post Post @relation(fields: [postId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}
2.9 onDelete / onUpdate(参照整合性)
model Comment {
id Int @id @default(autoincrement())
postId Int
post Post @relation(
fields: [postId],
references: [id],
onDelete: Cascade, // 親の削除に連動して子も削除
onUpdate: Cascade
)
}
3. Prisma Migrate でスキーマを DB に反映する
3.1 開発時のマイグレーション(migrate dev)
# schema.prisma を編集 → diff からマイグレーション SQL を生成し DB に適用
npx prisma migrate dev --name init
これで prisma/migrations/<timestamp>_init/migration.sql が生成され、自動で prisma generate も走ります。
3.2 本番デプロイ時(migrate deploy)
# 生成済みのマイグレーション SQL を本番 DB に流すだけ。新しい差分は作らない。
npx prisma migrate deploy
3.3 開発 DB を初期化したいとき(migrate reset)
# DB をドロップして再マイグレーション → seed まで自動実行
npx prisma migrate reset
3.4 マイグレーションを作らず即反映(db push)
# プロトタイプや一時 DB 用。マイグレーション履歴は残らないので本番では非推奨。
npx prisma db push
3.5 Prisma Client の再生成
# schema を変えたら必ず実行(型定義が更新される)
npx prisma generate
4. Prisma Client で CRUD・検索・ページネーション
4.1 Client のインスタンス化
// src/prisma.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient({
log: ["query", "error", "warn"], // 開発時はクエリログを出す
});
4.2 create / createMany
import { prisma } from "./prisma.js";
// 1 件作成
const user = await prisma.user.create({
data: { email: "alice@example.com", name: "Alice" },
});
// 複数件まとめて作成(MySQL/Postgres)
await prisma.user.createMany({
data: [
{ email: "bob@example.com" },
{ email: "carol@example.com" },
],
skipDuplicates: true, // unique 衝突をスキップ
});
4.3 findUnique / findFirst / findMany
// 主キー / unique での 1 件取得(最速)
const u1 = await prisma.user.findUnique({ where: { email: "alice@example.com" } });
// 条件に合う最初の 1 件
const u2 = await prisma.user.findFirst({ where: { name: { startsWith: "A" } } });
// 一覧取得
const list = await prisma.user.findMany({
where: { name: { not: null } },
orderBy: { createdAt: "desc" },
take: 20,
skip: 0,
});
4.4 update / updateMany / upsert
// 1 件更新(主キー指定)
await prisma.user.update({
where: { id: 1 },
data: { name: "Alice Updated" },
});
// 条件に合う全件を一括更新
await prisma.user.updateMany({
where: { name: null },
data: { name: "(no name)" },
});
// 存在すれば update / なければ create
await prisma.user.upsert({
where: { email: "dave@example.com" },
update: { name: "Dave v2" },
create: { email: "dave@example.com", name: "Dave" },
});
4.5 delete / deleteMany
await prisma.user.delete({ where: { id: 99 } });
await prisma.post.deleteMany({
where: { authorId: 1, isPublished: false },
});
4.6 複雑な where 条件(AND / OR / NOT)
const result = await prisma.post.findMany({
where: {
AND: [
{ isPublished: true },
{
OR: [
{ title: { contains: "Prisma", mode: "insensitive" } },
{ tags: { has: "orm" } }, // Postgres 配列
],
},
{ NOT: { authorId: 0 } },
],
},
});
4.7 select で必要なカラムだけ取得
// 戻り値の型も select に合わせて自動で絞られる
const slim = await prisma.user.findMany({
select: { id: true, email: true },
});
// slim: { id: number; email: string }[]
4.8 include で関連データを JOIN
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { isPublished: true },
orderBy: { createdAt: "desc" },
take: 5,
},
profile: true,
},
});
4.9 orderBy / take / skip(オフセット pagination)
const page = 3;
const perPage = 20;
const posts = await prisma.post.findMany({
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
take: perPage,
skip: (page - 1) * perPage,
});
4.10 cursor pagination(大規模データで推奨)
// 直前のページの末尾 ID を cursor に渡す
async function fetchPage(cursorId?: number) {
return prisma.post.findMany({
take: 20,
...(cursorId ? { cursor: { id: cursorId }, skip: 1 } : {}),
orderBy: { id: "desc" },
});
}
4.11 nested write で作成と同時に子レコードを作る
await prisma.user.create({
data: {
email: "eve@example.com",
posts: {
create: [
{ title: "First post" },
{ title: "Second post" },
],
},
profile: { create: { bio: "Hello Prisma" } },
},
include: { posts: true, profile: true },
});
4.12 既存レコードを connect / connectOrCreate
// 既存ユーザーを著者として接続
await prisma.post.create({
data: {
title: "New post",
author: { connect: { email: "alice@example.com" } },
},
});
// 多対多で「あればつなぐ・無ければ作る」
await prisma.postOnTag.create({
data: {
post: { connect: { id: 1 } },
tag: {
connectOrCreate: {
where: { name: "typescript" },
create: { name: "typescript" },
},
},
},
});
5. transaction と raw SQL
5.1 sequential transaction(配列を順に実行)
// 配列に並べたクエリを全て成功した時だけコミット
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: "frank@example.com" } }),
prisma.post.create({ data: { title: "hi", authorId: 1 } }),
]);
5.2 interactive transaction(コールバック・条件分岐可)
// 残高チェックして送金、のような途中で条件分岐するロジック
await prisma.$transaction(async (tx) => {
const from = await tx.account.findUniqueOrThrow({ where: { id: 1 } });
if (from.balance < 1000) throw new Error("残高不足");
await tx.account.update({ where: { id: 1 }, data: { balance: { decrement: 1000 } } });
await tx.account.update({ where: { id: 2 }, data: { balance: { increment: 1000 } } });
}, {
maxWait: 5000,
timeout: 10000,
isolationLevel: "Serializable", // 競合を厳格に検知
});
5.3 transaction 中の例外は自動 ROLLBACK
try {
await prisma.$transaction(async (tx) => {
await tx.user.create({ data: { email: "g@example.com" } });
throw new Error("意図的に失敗"); // ここで自動ロールバック
});
} catch (e) {
console.error("失敗したのでロールバックされた", e);
}
5.4 $queryRaw(SELECT 文・型付き)
type Row = { id: number; email: string };
// テンプレートリテラル内のパラメータは自動で prepared statement に
const rows = await prisma.$queryRaw<Row[]>`
SELECT id, email FROM "User" WHERE email LIKE ${"%@example.com"}
`;
5.5 $executeRaw(UPDATE/DELETE)
const affected = await prisma.$executeRaw`
UPDATE "User" SET name = ${"匿名"} WHERE name IS NULL
`;
console.log(`updated ${affected} rows`);
5.6 動的 SQL は Prisma.sql でビルド
import { Prisma } from "@prisma/client";
const onlyPublished = true;
const where = onlyPublished
? Prisma.sql`WHERE "isPublished" = true`
: Prisma.empty;
const rows = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM "Post" ${where} ORDER BY id DESC LIMIT 10
`;
6. Client Extensions・logging・seed・Studio
6.1 query 拡張で自動ソフトデリート
// すべての findMany に "deletedAt IS NULL" を自動付与
import { PrismaClient } from "@prisma/client";
const base = new PrismaClient();
export const prisma = base.$extends({
query: {
$allModels: {
async findMany({ args, query }) {
args.where = { ...args.where, deletedAt: null };
return query(args);
},
},
},
});
6.2 model 拡張でカスタムメソッド追加
export const prisma = base.$extends({
model: {
user: {
async softDelete(id: number) {
return base.user.update({ where: { id }, data: { deletedAt: new Date() } });
},
},
},
});
await prisma.user.softDelete(1); // 型補完が効く
6.3 result 拡張で計算プロパティを追加
export const prisma = base.$extends({
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(u) {
return `${u.firstName} ${u.lastName}`;
},
},
},
},
});
6.4 ログをイベントとして取得する
const prisma = new PrismaClient({
log: [
{ emit: "event", level: "query" },
{ emit: "stdout", level: "warn" },
{ emit: "stdout", level: "error" },
],
});
prisma.$on("query", (e) => {
console.log(`[${e.duration}ms] ${e.query} | params=${e.params}`);
});
6.5 prisma studio で GUI から確認
# ブラウザでテーブルを表示・編集できるローカル GUI
npx prisma studio
# http://localhost:5555
6.6 seed スクリプトの登録
// package.json
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
6.7 faker.js を使った seed 実装
npm i -D @faker-js/faker
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
import { faker } from "@faker-js/faker";
const prisma = new PrismaClient();
async function main() {
await prisma.post.deleteMany();
await prisma.user.deleteMany();
for (let i = 0; i < 30; i++) {
await prisma.user.create({
data: {
email: faker.internet.email(),
name: faker.person.fullName(),
posts: {
create: Array.from({ length: 3 }).map(() => ({
title: faker.lorem.sentence(),
isPublished: faker.datatype.boolean(),
})),
},
},
});
}
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
6.8 seed の実行
npx prisma db seed
7. Next.js / Edge Runtime / Pulse でフルスタック運用
7.1 Next.js 開発時のホットリロード問題
Next.js の開発サーバーはファイル変更のたびにモジュールを再ロードします。new PrismaClient() を毎回作ると コネクションが枯渇し「too many connections」エラーになります。Singleton 化が必須です。
7.2 Singleton パターン(必須)
// 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", "error"] : ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
7.3 Server Component から呼ぶ
// app/posts/page.tsx
import { prisma } from "@/lib/prisma";
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { isPublished: true },
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
7.4 Server Actions で書き込みも安全に
// app/posts/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
await prisma.post.create({
data: { title: String(formData.get("title")), authorId: 1 },
});
revalidatePath("/posts");
}
7.5 Edge では普通の Client が動かない理由
Vercel Edge や Cloudflare Workers は Node.js API が使えず TCP も張れないため、従来の Prisma Client(Rust エンジン + TCP)は動きません。解決策は 2 つで、Prisma Accelerate(HTTPS プロキシ + グローバルキャッシュ)を使うか、Driver Adapters(@prisma/adapter-neon など)で HTTP/WebSocket ベースの接続にすることです。
7.6 Accelerate の導入
npm i @prisma/extension-accelerate
// Edge / Workers でも動く Prisma Client
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
export const prisma = new PrismaClient().$extends(withAccelerate());
// クエリ単位でキャッシュ
const posts = await prisma.post.findMany({
where: { isPublished: true },
cacheStrategy: { ttl: 60, swr: 60 }, // 60 秒 fresh / 60 秒 stale-while-revalidate
});
7.7 Neon Driver Adapter で直接接続
npm i @prisma/adapter-neon @neondatabase/serverless
// schema.prisma に previewFeatures = ["driverAdapters"] を追加した上で
import { PrismaClient } from "@prisma/client";
import { PrismaNeon } from "@prisma/adapter-neon";
import { Pool } from "@neondatabase/serverless";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
export const prisma = new PrismaClient({ adapter });
7.8 Prisma Pulse でリアルタイム購読
Prisma Pulse は DB の変更を サーバ側で購読するためのマネージドサービスです。Postgres の論理レプリケーションを使って create / update / delete のストリームを受け取り、リアルタイムなダッシュボードや通知の実装に使えます。
import { PrismaClient } from "@prisma/client";
import { withPulse } from "@prisma/extension-pulse";
const prisma = new PrismaClient().$extends(
withPulse({ apiKey: process.env.PULSE_API_KEY! })
);
const subscription = await prisma.post.subscribe({
create: { after: { isPublished: true } },
});
for await (const event of subscription) {
console.log("新しい公開記事:", event.created.title);
}
8. テスト戦略(testcontainers + Vitest)
8.1 必要パッケージ
npm i -D vitest testcontainers @testcontainers/postgresql
8.2 setup ファイル(Postgres を立ち上げて DATABASE_URL を差し替え)
// tests/setup.ts
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { execSync } from "node:child_process";
let container: any;
export async function setup() {
container = await new PostgreSqlContainer("postgres:16-alpine").start();
process.env.DATABASE_URL = container.getConnectionUri();
execSync("npx prisma migrate deploy", {
env: { ...process.env, DATABASE_URL: process.env.DATABASE_URL },
stdio: "inherit",
});
}
export async function teardown() {
await container?.stop();
}
8.3 テスト本体
// tests/post.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { PrismaClient } from "@prisma/client";
let prisma: PrismaClient;
beforeAll(() => {
prisma = new PrismaClient();
});
afterAll(async () => {
await prisma.$disconnect();
});
describe("Post", () => {
it("作成して取得できる", async () => {
const u = await prisma.user.create({ data: { email: `${Date.now()}@t.com` } });
const p = await prisma.post.create({ data: { title: "hi", authorId: u.id } });
const found = await prisma.post.findUnique({ where: { id: p.id } });
expect(found?.title).toBe("hi");
});
});
8.4 ユニットテストで Prisma をモックする戦略
npm i -D vitest-mock-extended
// tests/repo.unit.test.ts
import { mockDeep, DeepMockProxy } from "vitest-mock-extended";
import { PrismaClient } from "@prisma/client";
const prisma = mockDeep<PrismaClient>() as DeepMockProxy<PrismaClient>;
it("findMany を 1 回呼ぶ", async () => {
prisma.post.findMany.mockResolvedValue([]);
await prisma.post.findMany({});
expect(prisma.post.findMany).toHaveBeenCalledOnce();
});
9. Drizzle / TypeORM / Kysely との比較とパフォーマンス・本番運用
9.1 Drizzle ORM との比較
Drizzle は SQL に限りなく近い書き味と Edge Runtime での起動の速さが特徴です。schema.ts を TypeScript で書き、クエリは SQL DSL で組み立てます。Prisma が「宣言的スキーマ + 高レベル API」なのに対し、Drizzle は「TS で書く薄い SQL ビルダー」という位置づけです。
// Drizzle のクエリ例(参考)
import { eq } from "drizzle-orm";
import { db, users } from "./schema";
const u = await db.select().from(users).where(eq(users.email, "a@example.com"));
// 同じクエリを Prisma で書くと
const u = await prisma.user.findUnique({ where: { email: "a@example.com" } });
選定基準:大規模アプリ・チーム開発で「schema を一元管理しマイグレーションも自動」したいなら Prisma、Edge / Workers / 軽量サーバで「起動の速さ・バンドルサイズ・SQL の細かい制御」を優先するなら Drizzle。
9.2 TypeORM との比較
TypeORM は デコレータベースのクラシック ORM で、NestJS と組み合わせる文化が長く根付いています。半面、Active Record / DataMapper 両モードの混在、relation 周りのバグ、メンテナンスペースの遅さがしばしば指摘されます。新規プロジェクトで NestJS を採用する場合でも Prisma を選ぶケースが増えています。
// TypeORM のエンティティ(参考)
@Entity()
class User {
@PrimaryGeneratedColumn() id!: number;
@Column({ unique: true }) email!: string;
@OneToMany(() => Post, (p) => p.author) posts!: Post[];
}
9.3 Kysely との比較
Kysely は 型安全な SQL クエリビルダーに振り切ったライブラリで、自前で Database 型を定義すれば JOIN / サブクエリまで完全に型付きで書けます。マイグレーション機能は最小限なので、Prisma で schema 管理 → ランタイムは Kysely という併用構成もしばしば見られます。
// Kysely のクエリ例(参考)
const rows = await db
.selectFrom("user")
.select(["id", "email"])
.where("email", "=", "a@example.com")
.execute();
9.4 選定フローチャート
- schema を
schema.prismaで一元管理し、migration を自動生成したい - 関係取得を
includeで素早く書きたい(nested write を含む) - チームメンバーの SQL 練度に差があり、レビュー負荷を下げたい
Drizzle / Kysely を検討すべきケース
- Cloudflare Workers など Edge ランタイムでバンドルサイズを最小化したい
- 複雑な分析クエリ・ウィンドウ関数を SQL 寄りに書きたい
9.5 Connection Pooling の基本
Prisma Client は内部にコネクションプールを持っており、connection_limit を URL で調整できます。サーバレス環境では 1 つのリクエスト = 1 つの関数インスタンスになりがちで、すぐに「too many connections」を起こします。
# 単一プロセス用に明示的に上限を絞る
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=10"
9.6 PgBouncer の前段(サーバレス必須)
# PgBouncer 経由のときは pgbouncer=true を必ず付ける
DATABASE_URL="postgresql://user:pass@bouncer:6432/db?pgbouncer=true&connection_limit=1"
PgBouncer の transaction pooling を使う場合、Prisma のプリペアドステートメントと衝突しないよう pgbouncer=true が必須です。
9.7 N+1 を防ぐ include / 集計
// NG: 1 件ずつ count を取るとリレーション数だけクエリが発生する
// OK: _count を使えば 1 クエリで件数まで取れる
const users = await prisma.user.findMany({
include: { _count: { select: { posts: true } } },
});
9.8 大量レコードを分割して取得(batched cursor)
async function* iterateAllPosts(batch = 1000) {
let cursor: number | undefined;
while (true) {
const rows = await prisma.post.findMany({
take: batch,
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
orderBy: { id: "asc" },
});
if (rows.length === 0) return;
yield rows;
cursor = rows[rows.length - 1].id;
}
}
for await (const batch of iterateAllPosts()) {
// 1000 件ずつ処理してメモリ枯渇を防ぐ
}
9.9 PostgreSQL extensions(pgvector など)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgvector(map: "vector")]
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
model Doc {
id Int @id @default(autoincrement())
body String
embedding Unsupported("vector(1536)")?
}
9.10 schema を分割して保守性を上げる
// schema.prisma に下記を入れると prisma/schema/*.prisma に分割できる
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
}
9.11 Prisma の例外クラスとエラーコード早見表
import { Prisma } from "@prisma/client";
try {
await prisma.user.create({ data: { email: "dup@example.com" } });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
// unique 制約違反
console.error("メールアドレスが重複しています:", e.meta?.target);
}
} else if (e instanceof Prisma.PrismaClientValidationError) {
console.error("引数の型が間違っている", e.message);
} else {
throw e;
}
}
- P2002:Unique 制約違反
- P2003:外部キー制約違反
- P2025:対象レコードが存在しない(update/delete 時)
- P1001:DB に接続できない(URL ミス・到達性)
- P2024:プール待ちのタイムアウト(connection_limit 不足)
10. まとめ・関連サービス・キャリア
Prisma の使い方を schema 設計 → migration → CRUD → relation → transaction → raw SQL → 拡張 → Next.js / Edge → テスト → 本番運用 まで一気通貫で見てきました。Drizzle / Kysely と比較すると、Prisma の強みは 「型・schema・migration・Client」を 1 つのエコシステムで完結させられること。新規プロジェクトの ORM 選定で迷っているなら、まず Prisma で立ち上げて、Edge やパフォーマンスのボトルネックが出たら部分的に Driver Adapter や raw SQL を併用するのが現実的な最適解です。
Prisma 単体ではなく、TypeScript・Node.js・SQL・REST/GraphQL を横断で身につけると現場で即戦力です。短期集中で叩き込みたい人には以下のサービスが定番です。
- テックアカデミー:Node.js / TypeScript / SQL を含む Web 開発カリキュラム(オンライン完結)
- 侍エンジニア:マンツーマンで Prisma + Next.js の API を成果物にできる
- DMM WEBCAMP:Web エンジニア転職を見据えた長期コース(バックエンド志望に強い)
- レバテックキャリア:Prisma / TypeScript / Node.js の実務求人と年収レンジを確認しキャリア設計

コメント