Prisma完全実践ガイド〜schema・migration・relation・transaction・Drizzle比較【2026年版】〜

「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. 1. Prisma の概要と Node.js 22 環境セットアップ
    1. 1.1 Prisma とは何か(3 つのコンポーネント)
    2. 1.2 Prisma 5 系で押さえるべき変更点
    3. 1.3 Node.js / npm のバージョン確認
    4. 1.4 プロジェクト初期化
    5. 1.5 Prisma のインストール
    6. 1.6 prisma init で初期化
  2. 2. schema.prisma 完全解剖とリレーション設計
    1. 2.1 最小の schema.prisma
    2. 2.2 データソース別の書き方(4 種類)
    3. 2.3 主要属性(@id / @unique / @default / @relation)
    4. 2.4 enum 定義
    5. 2.5 ID 戦略の選び方
    6. 2.6 1 対多(最も基本)
    7. 2.7 1 対 1(プロフィール拡張)
    8. 2.8 多対多(明示的な中間テーブル)
    9. 2.9 onDelete / onUpdate(参照整合性)
  3. 3. Prisma Migrate でスキーマを DB に反映する
    1. 3.1 開発時のマイグレーション(migrate dev)
    2. 3.2 本番デプロイ時(migrate deploy)
    3. 3.3 開発 DB を初期化したいとき(migrate reset)
    4. 3.4 マイグレーションを作らず即反映(db push)
    5. 3.5 Prisma Client の再生成
  4. 4. Prisma Client で CRUD・検索・ページネーション
    1. 4.1 Client のインスタンス化
    2. 4.2 create / createMany
    3. 4.3 findUnique / findFirst / findMany
    4. 4.4 update / updateMany / upsert
    5. 4.5 delete / deleteMany
    6. 4.6 複雑な where 条件(AND / OR / NOT)
    7. 4.7 select で必要なカラムだけ取得
    8. 4.8 include で関連データを JOIN
    9. 4.9 orderBy / take / skip(オフセット pagination)
    10. 4.10 cursor pagination(大規模データで推奨)
    11. 4.11 nested write で作成と同時に子レコードを作る
    12. 4.12 既存レコードを connect / connectOrCreate
  5. 5. transaction と raw SQL
    1. 5.1 sequential transaction(配列を順に実行)
    2. 5.2 interactive transaction(コールバック・条件分岐可)
    3. 5.3 transaction 中の例外は自動 ROLLBACK
    4. 5.4 $queryRaw(SELECT 文・型付き)
    5. 5.5 $executeRaw(UPDATE/DELETE)
    6. 5.6 動的 SQL は Prisma.sql でビルド
  6. 6. Client Extensions・logging・seed・Studio
    1. 6.1 query 拡張で自動ソフトデリート
    2. 6.2 model 拡張でカスタムメソッド追加
    3. 6.3 result 拡張で計算プロパティを追加
    4. 6.4 ログをイベントとして取得する
    5. 6.5 prisma studio で GUI から確認
    6. 6.6 seed スクリプトの登録
    7. 6.7 faker.js を使った seed 実装
    8. 6.8 seed の実行
  7. 7. Next.js / Edge Runtime / Pulse でフルスタック運用
    1. 7.1 Next.js 開発時のホットリロード問題
    2. 7.2 Singleton パターン(必須)
    3. 7.3 Server Component から呼ぶ
    4. 7.4 Server Actions で書き込みも安全に
    5. 7.5 Edge では普通の Client が動かない理由
    6. 7.6 Accelerate の導入
    7. 7.7 Neon Driver Adapter で直接接続
    8. 7.8 Prisma Pulse でリアルタイム購読
  8. 8. テスト戦略(testcontainers + Vitest)
    1. 8.1 必要パッケージ
    2. 8.2 setup ファイル(Postgres を立ち上げて DATABASE_URL を差し替え)
    3. 8.3 テスト本体
    4. 8.4 ユニットテストで Prisma をモックする戦略
  9. 9. Drizzle / TypeORM / Kysely との比較とパフォーマンス・本番運用
    1. 9.1 Drizzle ORM との比較
    2. 9.2 TypeORM との比較
    3. 9.3 Kysely との比較
    4. 9.4 選定フローチャート
    5. 9.5 Connection Pooling の基本
    6. 9.6 PgBouncer の前段(サーバレス必須)
    7. 9.7 N+1 を防ぐ include / 集計
    8. 9.8 大量レコードを分割して取得(batched cursor)
    9. 9.9 PostgreSQL extensions(pgvector など)
    10. 9.10 schema を分割して保守性を上げる
    11. 9.11 Prisma の例外クラスとエラーコード早見表
  10. 10. まとめ・関連サービス・キャリア
    1. 関連記事

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 選定フローチャート

Prisma を選ぶべきケース

  • 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 の実務求人と年収レンジを確認しキャリア設計

関連記事

コメント

タイトルとURLをコピーしました