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がbulk updateで更新されない、ソフトデリートとfindUniqueOrThrowの組み合わせで削除済みレコードが漏れるなど、よくあるアンチパターンとその回避策を具体的に解説。さらに、Prismaのバージョン間でのAPIの違い(relationJoins、omitフィールド修飾子、クライアント拡張、アダプターベースの初期化など)にも対応し、環境に応じた正しい実装方法を示しています。このスキルは、Claude Code、Coder、Cursor、Copilot、Codex、Windsurf、Aider、Continue.devなど、任意のAIエージェントが開発者のPrisma使用時における課題解決を支援するために活用できます。

対応~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/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にマッピングして公開フィールドを制御する:

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

// 良い例: 明示的な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クエリが発行される。

// 悪い例: N+1 — ユーザーごとに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 } });

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('メールアドレスは既に存在します');
    if (e.code === 'P2025') throw new NotFoundError('レコードが見つかりません');
    if (e.code === 'P2003') throw new BadRequestError('参照先レコードが存在しません');
  }
  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はレコードではなく件数(count)を返す

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

// 良い例: 最初に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秒でタイムアウトする

// 悪い例: トランザクション内の外部呼び出しがデフォルトの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 } });
});

// 良い例: 外部呼び出しはトランザクションの外で実行
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でのみ自動設定される。バルク書き込みでは古い値のままになる。

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

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

ソフトデリート + findUniqueOrThrowは削除済みレコードを漏洩する

findUniqueOrThrowは、行がDBに存在しない場合にのみP2025をスローする。ソフトデリートされた行はまだ存在するため、エラーなく返される。

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

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

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

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

deleteManyをwhereなしで使用するとすべての行を削除する

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

// 良い例
await prisma.post.deleteMany({ where: { authorId: userId } });

ベストプラクティス

ルール理由
CI/CDではmigrate deploy、migrate devはローカルのみmigrate devはドリフト時にDBをリセットする可能性がある
エンティティをレスポンスDTOにマッピング内部フィールドの漏洩を防止
すべてのトランザクションのインタラクティブ形式にタイムアウトを設定デフォルトの5秒制限は外部呼び出しで簡単に超過する
ソフトデリートテーブルではfindUniqueOrThrowではなくfindFirstOrThrowfindUniqueOrThrowは一意制約を強制し、deletedAtと組み合わせられない
DB URLにクエリパラメータを埋め込む(DATABASE_URL=postgresql://...?connection_limit=1)文字列連結は既存のクエリパラメータを壊す
フィード/リストには常にカーソルページネーションを使用安定したページネーション、大規模オフセットでのパフォーマンス問題なし
includeを使用する場合はselectでリレーションフィールドを制限リレーションからの過剰フェッチを防止、N+1を回避
外部呼び出しをトランザクションの外に保持トランザクションのタイムアウトとデッドロックを防止

クイックリファレンス

# バージョンチェック
npx prisma --version

# 安全なマイグレーション
npx prisma migrate deploy

# ドリフトチェック(適用なし)
npx prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url "$SHADOW_DATABASE_URL"

# クライアントの再生成
npx prisma generate

# スキーマのフォーマット
npx prisma format
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

// アダプターベースの初期化(新しいPrismaインストールに推奨)
const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
# サーバーレスデータベースURL(PgBouncer対応)
DATABASE_URL="postgresql://user:pass@host:6543/db?pgbouncer=true&connection_limit=1&pool_timeout=20"

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

関連スキル