Communitygithub.com

Prisma パターン

TypeScript バックエンド向け Prisma ORM パターン集 — スキーマ設計、クエリ最適化、トランザクション、ページネーション、および updateMany の戻り値、$transaction タイムアウト、migrate dev による DB リセット、一括書き込み時の @updatedAt スキップ、サーバーレス接続枯渇などの重要な落とし穴をカバーします。Prisma スキーマやクエリを書くとき、トランザクション、マイグレーション、サーバーレス接続制限をデバッグするときに使用します。

Prisma パターン とは?

このスキルは、TypeScript バックエンドで Prisma ORM を使用する開発者のために設計された実践的なパターンとアンチパターンのリファレンスです。スキーマ設計(ID 戦略、インデックス、ソフトデリート)、クエリ最適化(include vs select、N+1 問題の回避)、トランザクション(配列形式とインタラクティブ形式の使い分け)、ページネーション(カーソルベース)、エラーハンドリング、サーバーレス環境での接続プール設定を網羅しています。さらに、`updateMany` が件数のみ返す、`$transaction` インタラクティブ形式の 5 秒タイムアウト、`migrate dev` によるデータリセットリスク、一括更新での `@updatedAt` の非動作、ソフトデリートと `findUniqueOrThrow` の非互換など、Prisma 特有の落とし穴を詳しく解説します。このスキルは、Claude Code、Cursor、Codex などの AI エージェントがバックエンド開発タスクで Prisma の安全で効率的な使い方を支援するために活用できます。

対応~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/prisma-patterns

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Prisma パターン は何をしますか?

TypeScript バックエンドにおける Prisma ORM のプロダクションパターンと非自明な落とし穴。

パターンを適用する前にバージョンを確認してください。 Prisma API の表面はメジャーリリースごとに変化しています。

npx prisma --version

バージョン間の注目すべき API の違い:

  • relationJoins はリレーションを個別クエリではなく JOIN で読み込めますが、大量の 1:N リレーションや深い include で行爆発を起こす可能性があります — 両方のアプローチをベンチマークしてください
  • omit フィールド修飾子と prisma.$extends クライアント拡張 API が追加されました
  • 新しいインストール: パッケージ名が @prisma/client ではなく prisma になっている場合があります。PrismaClient はドライバアダプター(例:@prisma/adapter-pg)を必要とする場合があります。datasource.url が schema.prisma ではなく prisma.config.ts にある場合があります
  • CLI コマンド(migrate dev、migrate deploy、generate)はバージョン間で変更されていません

アクティベーション条件

  • Prisma スキーマのモデルとリレーションを設計または変更するとき
  • クエリ、トランザクション、またはページネーションロジックを書くとき
  • updateMany、deleteMany、または一括操作を使用するとき
  • データベースマイグレーションを実行または計画するとき
  • サーバーレス環境(Vercel、Lambda、Cloudflare Workers)にデプロイするとき
  • ソフトデリートまたはマルチテナント行フィルタリングを実装するとき

コアコンセプト

ID 戦略

戦略使用するケース避けるべきケース
@default(cuid())デフォルトの選択肢 — URL セーフ、ソート可能、衝突なし外部システムで連番 ID が必要な場合
@default(uuid())Prisma 以外のシステムとの相互運用性が必要な場合書き込みの多いテーブル(ランダム UUID は B-tree インデックスを断片化)
@default(autoincrement())内部結合テーブル、監査ログ公開向け ID(レコード数を露出)

スキーマのデフォルト

model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique は自動でインデックスを作成 — @@index は不要
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // ソフトデリート+ソートクエリ用複合インデックス
}
  • すべての外部キーおよび WHERE や ORDER BY で使用するカラムに @@index を追加します。
  • ソフトデリートが将来的に必要になりそうな場合は、事前に deletedAt DateTime? を宣言してください — 後から追加すると、稼働中のテーブルにマイグレーションが必要になります。
  • updatedAt @updatedAt は、update と upsert でのみ Prisma によって自動設定されます(一括更新の落とし穴についてはアンチパターンを参照)。

include vs select

includeselect
戻り値すべてのスカラーフィールド+指定されたリレーション指定されたフィールドのみ
使用するケースほとんどのフィールドとリレーションが必要な場合ホットパス、大きなテーブル、過剰取得の回避
パフォーマンス幅広いテーブルで過剰取得の可能性あり最小限のペイロード、大きなデータセットで高速
Prisma 5 での注意点デフォルトで JOIN を使用(relationJoins)同じ
// include — すべてのカラム+リレーション
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — 明示的な許可リスト
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});

API レスポンスから生の Prisma エンティティを決して返さないでください — レスポンス DTO にマッピングして公開フィールドを制御します:

// BAD: passwordHash、deletedAt、内部フィールドが漏洩する
return await prisma.user.findUniqueOrThrow({ where: { id } });

// GOOD: 明示的な DTO マッピング
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };

トランザクション形式の選択

状況使用する形式
独立した操作、相互依存なし配列形式
後のステップが前の結果に依存インタラクティブ形式
外部呼び出し(メール、HTTP)が含まれるトランザクションの外側
// 配列形式 — 1 ラウンドトリップでバッチ処理
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// インタラクティブ形式 — tx クライアントのみ使用し、外側の prisma クライアントは決して使用しない
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title, authorId: user.id } });
});

PrismaClient シングルトン

各 PrismaClient インスタンスは独自の接続プールを開きます。1 回だけインスタンス化します。

// lib/prisma.ts

// オプション A — アダプターベースの初期化(新しい Prisma インストールで必要)
import { PrismaClient } from '@prisma/client'; // またはセットアップに応じた生成クライアントパス
import { PrismaPg } from '@prisma/adapter-pg';

function createPrismaClient() {
  const adapter = new PrismaPg({
    connectionString: process.env.DATABASE_URL!,
  });
  return new PrismaClient({
    adapter,
    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  });
}

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

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

// オプション B — 直接初期化(古いインストール、アダプター不要)
// import { PrismaClient } from '@prisma/client';
// export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });

Prisma インストールが PrismaClient コンストラクタで adapter 引数を必要とする場合はオプション A を使用します。 new PrismaClient() が引数なしで動作する場合はオプション B を使用します。コンパイラにどちらが正しいかを判断させてください。

globalThis パターンは、ホットリロード中(Next.js、nodemon、ts-node-dev)の重複インスタンスを防止します。

N+1 問題

ループ内でリレーションを読み込むと、行ごとに 1 つのクエリが発行されます。

// BAD: N+1 — ユーザーごとに 1 つの余分なクエリ
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// GOOD: 単一クエリ
const users = await prisma.user.findMany({ include: { posts: true } });

Prisma 5+ の relationJoins では、include 形式は単一の JOIN を使用します。大規模な 1:N セットでは結果セットのサイズが増加する可能性があります — リレーションが親ごとに多くの行を返す可能性がある場合は、両方のアプローチをベンチマークしてください。

コード例

カーソルページネーション(フィードや大規模データセットに推奨)

async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // 二次ソートにより、重複タイムスタンプでの不安定なページネーションを防止
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}

limit + 1 を取得してポップ — 余分なカウントクエリなしで hasNextPage を検出する正規の方法です。複数の行が同じタイムスタンプを共有する場合に不安定なページネーションを防ぐために、一意のフィールド(例:id)を二次的な orderBy として常に含めてください。ユーザーが任意のページにジャンプする必要がある場合(管理テーブル)にのみ、オフセットページネーションを使用します。

ソフトデリート

// 常に明示的にフィルタリング — ミドルウェアに依存しない(動作を隠蔽し、デバッグが困難)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // 復元

エラーハンドリング

import { Prisma } from '@prisma/client'; // またはセットアップに応じた生成クライアントパス

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('Email already exists');
    if (e.code === 'P2025') throw new NotFoundError('Record not found');
    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');
  }
  throw e;
}

よくあるコード: P2002 一意制約違反 · P2025 見つからない · P2003 外部キー違反。

サービス境界でキャッチしてドメインエラーに変換します。生の Prisma メッセージを API コンシューマーに公開しないでください。

接続プール — サーバーレス

接続パラメータを DATABASE_URL に直接埋め込みます — 文字列連結は、URL に既にクエリパラメータがある場合(例:?schema=public)に壊れます:

# .env — 推奨: URL にパラメータを埋め込む
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"

# 外部プーラー(PgBouncer、Supabase pooler)を使用する場合
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
// Vercel、AWS Lambda、および同様のサーバーレスランタイム:
// インスタンスごとにプールを 1 に制限。connection_limit と pool_timeout は DATABASE_URL で制御

// アダプターベースのセットアップ(Prisma インストールがアダプターを必要とする場合):
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});

// 直接セットアップ(Prisma インストールがアダプターを必要としない場合):
// const prisma = new PrismaClient();

アンチパターン

updateMany はレコードではなく件数を返す

// BAD: 結果は { count: 2 } — users[0] は undefined
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// GOOD: 最初に ID を取得し、次に更新し、最後に影響を受けた行のみを取得
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });

deleteMany も同様 — { count: n } を返し、削除された行は決して返しません。

$transaction インタラクティブ形式は 5 秒でタイムアウトする

// BAD: トランザクション内の外部呼び出しがデフォルトの 5 秒を超える → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // 外部呼び出し
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// GOOD: 外部呼び出しはトランザクションの外側
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// バルク処理で本当に必要な場合のみタイムアウトを引き上げる
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });

migrate dev はデータベースをリセットする可能性がある

migrate dev はスキーマのドリフトを検出し、DB のリセットを促す場合があり、すべてのデータが失われます。

# 共有開発環境、ステージング、または本番環境では絶対に使用しない
npx prisma migrate dev --name add_column

# ローカルの単独開発以外では安全
npx prisma migrate deploy

# 適用せずにドリフトを確認
npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "$SHADOW_DATABASE_URL"

マイグレーションファイルを手動で編集すると将来のデプロイが壊れる

Prisma はすべてのマイグレーションファイルをチェックサムで管理します。適用後に編集すると、元のマイグレーションがすでに実行されたすべての環境で P3006 checksum mismatch が発生します。代わりに新しいマイグレーションを作成してください。

破壊的なスキーマ変更には複数ステップのマイグレーションが必要

既存のカラムに NOT NULL を追加したり、カラム名を変更するマイグレーションを 1 回で行うと、テーブルがロックされたりデータが失われます。拡張と縮小(expand-and-contract)を使用します:

# ステップ 1: ローカルでマイグレーションを作成し、デプロイ
npx prisma migrate dev --name add_new_column   # ローカルのみ
npx prisma migrate deploy                       # ステージング / 本番
// ステップ 2: データのバックフィル(スクリプトまたはマイグレーションジョブで実行。シェルでは実行しない)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
# ステップ 3: NOT NULL 制約のマイグレーションをローカルで作成し、デプロイ
npx prisma migrate dev --name make_new_column_required  # ローカルのみ
npx prisma migrate deploy                               # ステージング / 本番

@updatedAt は updateMany では機能しない

@updatedAt は update と upsert でのみ自動設定されます。一括書き込みでは古い値のままになります。

// BAD: updatedAt は古い値のまま
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// GOOD
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});

ソフトデリート + findUniqueOrThrow は削除されたレコードを漏洩する

findUniqueOrThrow は、行が DB に存在しない場合にのみ P2025 をスローします。ソフトデリートされた行は依然として存在し、エラーなしで返されます。

findUniqueOrThrow は where に一意制約フィールドを必要とします — id と一緒に deletedAt: null を追加すると、{ id, deletedAt } は複合一意制約ではないため型エラーが発生します。代わりに findFirstOrThrow を使用します。

// BAD: ソフトデリートされたユーザーを返す
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// BAD: Prisma の型エラー — { id, deletedAt } は一意制約ではない
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// GOOD: findFirstOrThrow は任意の where 条件をサポート
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });

deleteMany に where がないとすべての行が削除される

// BAD: テーブルを静かに全削除
await prisma.post.deleteMany();

// GOOD
await prisma.post.deleteMany({ where: { authorId: userId } });

ベストプラクティス

ルール理由
CI/CD では migrate deploy、ローカルのみ migrate devmigrate dev はドリフト時に DB をリセットする可能性がある
エンティティをレスポンス DTO にマッピング内部フィールドの漏洩を防ぐ
ループ内でのクエリを避け、include を使用N+1 問題を防止
各 where 句に適切なインデックスを追加クエリパフォーマンスを確保 (EXPLAIN ANALYZE で確認)
@updatedAt は update/upsert のみ自動設定。updateMany では手動で設定一括更新後の updatedAt の古い値を防止
マイグレーションファイルを決して手動編集しないチェックサムエラー P3006 を防止
ソフトデリートモデルでは findUnique の代わりに findFirst/findFirstOrThrow を使用ソフトデリートされた行が findUniqueOrThrow で返されるのを防ぐ

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

関連スキル