Communitygithub.com

prisma-patterns

Prisma ORM 在 TypeScript 後端的生產模式 — schema 設計、查詢優化、交易、分頁及關鍵陷阱,如 updateMany 回傳計數而非記錄、$transaction 超時、migrate dev 重置資料庫、@updatedAt 在批量寫入時失效及無伺服器連線耗盡。在編寫 Prisma schema 或查詢、除錯交易、遷移或無伺服器連線限制時使用。

prisma-patterns 是什麼?

此技能提供 Prisma ORM 在 TypeScript 後端環境中的生產級模式與非顯而易見的陷阱。它涵蓋了 schema 設計、查詢優化、交易管理、分頁及軟刪除等核心概念,並特別關注諸如 `updateMany` 回傳計數而非記錄、`$transaction` 在 5 秒後超時、`migrate dev` 可能重置資料庫、`@updatedAt` 在批量寫入時失效以及無伺服器環境中的連線耗盡等關鍵問題。技能也詳細說明了 PrismaClient 實例化、N+1 問題、錯誤處理及安全的分頁策略,並提供清晰的程式碼範例來演示正確與錯誤的做法。此技能支援 Claude Code、Cursor 及 Codex 等 AI 代理平台,設計用於協助開發人員在編寫 Prisma schema 或查詢進行除錯時,能夠快速取得最佳實踐並避開已知陷阱。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/prisma-patterns

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Prisma Patterns

TypeScript 後端中 Prisma ORM 的生產模式與非顯而易見的陷阱。

在套用模式之前,請先檢查您的版本。 Prisma API 表面在主要版本之間有所演變:

npx prisma --version

各版本間值得注意的 API 差異:

  • relationJoins 可以透過 JOIN 而非個別查詢來載入關聯,但可能會導致大型 1:N 關聯或深度 include 時出現資料列爆炸 — 請對兩種方法進行基準測試
  • 新增了 omit 欄位修飾符和 prisma.$extends 客戶端擴展 API
  • 較新的安裝:套件名稱可能為 prisma 而非 @prisma/client;PrismaClient 可能需要驅動適配器(例如 @prisma/adapter-pg);datasource.url 可能位於 prisma.config.ts 而非 schema.prisma
  • CLI 指令(migrate dev、migrate deploy、generate)在各版本中保持不變

何時啟用

  • 設計或修改 Prisma schema 模型與關聯
  • 編寫查詢、交易或分頁邏輯
  • 使用 updateMany、deleteMany 或任何批量操作
  • 執行或規劃資料庫遷移
  • 部署至無伺服器環境(Vercel、Lambda、Cloudflare Workers)
  • 實作軟刪除或多租戶資料列過濾

核心概念

ID 策略

策略使用時機避免使用時機
@default(cuid())預設選擇 — URL 安全、可排序、無衝突外部系統需要循序 ID
@default(uuid())需要與非 Prisma 系統互通大量寫入的資料表(隨機 UUID 會碎片化 B-tree 索引)
@default(autoincrement())內部關聯表、稽核日誌公開 ID(會暴露記錄數量)

Schema 預設值

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 僅會由 Prisma 在 update 和 upsert 時自動設定(請參閱反模式中的批量更新陷阱)。

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 };

交易形式選擇

情境使用
獨立操作,無相互依賴陣列形式
後續步驟依賴先前的結果互動形式
涉及外部呼叫(Email、HTTP)完全在交易外部
// 陣列形式 — 在一個往返中批次處理
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('不允許');
  return tx.post.create({ data: { title, authorId: user.id } });
});

PrismaClient 單例

每個 PrismaClient 實例會開啟自己的連線池。請僅實例化一次。

// 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 問題

在迴圈內部載入關聯會對每個資料列執行一個查詢。

// 不好:N+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('Email 已存在');
    if (e.code === 'P2025') throw new NotFoundError('找不到記錄');
    if (e.code === 'P2003') throw new BadRequestError('參考的記錄不存在');
  }
  throw e;
}

常見代碼:P2002 唯一性違反 · P2025 找不到 · P2003 外鍵違反。

在服務邊界捕獲並轉換為領域錯誤。切勿向 API 消費者暴露原始的 Prisma 訊息。

連線池 — 無伺服器

將連線參數直接嵌入 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: 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 會偵測 schema 漂移,並可能提示重置資料庫,從而清除所有資料。

# 絕不在共用開發、暫存或生產環境中使用
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 校驗和不匹配。請改為建立一個新的遷移。

破壞性 Schema 變更需要多步驟遷移

在一個遷移中為現有欄位新增 NOT NULL 或重新命名欄位會鎖定資料表或刪除資料。請使用擴展和收縮模式:

# 步驟 1:在本機建立遷移,然後部署
npx prisma migrate dev --name add_new_column   # 僅本機
npx prisma migrate deploy                       # 暫存 / 生產
// 步驟 2:回填資料(在腳本或遷移作業中執行,而非在 shell 中)
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 僅在資料庫中不存在該資料列時才會拋出 P2025。軟刪除的資料列仍然存在,並且會無錯誤地回傳。

findUniqueOrThrow 需要在 where 中使用唯一限制式欄位 — 將 deletedAt: null 與 id 一起加入會中斷類型,因為 { 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 } });

沒有 where 的 deleteMany 會刪除每一行

// 不好:靜默清空資料表
await prisma.post.deleteMany();

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

最佳實踐

規則原因
在 CI/CD 中使用 migrate deploy,僅在本機使用 migrate devmigrate dev 可能會在發生漂移時重置資料庫
將實體對應到回應 DTO防止洩漏內部欄位

…

[截斷以利翻譯]

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.

相關技能