Communitygithub.com

prisma-patterns

Prisma ORM 在 TypeScript 後端中的生產模式——涵蓋 schema 設計、查詢優化、事務、分頁,以及常見陷阱,例如 updateMany 回傳計數而非記錄、$transaction 超時、migrate dev 重置資料庫、@updatedAt 在批量寫入時失效,以及無伺服器環境的連線耗盡問題。在撰寫 Prisma schema 或查詢、除錯事務、遷移或無伺服器連線限制時使用。

prisma-patterns 是什麼?

Prisma Patterns 是一份全面的指南,專為在 TypeScript 後端使用 Prisma ORM 的開發者設計。它涵蓋了從 schema 設計(ID 策略、預設值)、查詢優化(`include` vs `select`、N+1 問題)到進階模式(遊標分頁、軟刪除)的所有內容。該技能特別強調了 Prisma 中不明顯的陷阱和反模式,例如 `updateMany`/`deleteMany` 回傳計數而非實際記錄、`$transaction` 互動形式預設 5 秒超時、`migrate dev` 在 schema 漂移時可能重置資料庫、`@updatedAt` 在 `updateMany` 中不會自動更新,以及無伺服器環境中的連線耗盡問題。它提供了適用於不同 Prisma 版本(包括需要驅動程式配接器的較新安裝)的具體程式碼範例和最佳實踐。此技能可協助開發者避免常見錯誤,撰寫更可靠、高效的資料庫程式碼,並順利遷移至無伺服器部署。

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

在你喜歡的 AI 中提問

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

說明文件

Prisma 模式

用於 TypeScript 後端中 Prisma ORM 的生產模式與非直觀陷阱。

在套用模式之前,請先檢查您的 Prisma 版本。 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 僅在 update 和 upsert 時由 Prisma 自動設定(關於批量更新的陷阱,請參閱反模式)。

include 與 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)完全在事務外部
// 陣列形式 — 在一個往返中批次處理
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 實例會開啟自己的連線池。請僅實例化一次。

// 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 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 外鍵違規。

在服務邊界捕獲並轉換為領域錯誤。切勿向 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 會對每個遷移檔案進行 checksum 檢查。套用後編輯會導致原始遷移已執行的每個環境出現 P3006 checksum mismatch。請改為建立新的遷移。

破壞性 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 中使用唯一約束欄位——在 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 devmigrate dev 在漂移時可能重置資料庫
將實體對應到回應 DTO防止洩漏內部欄位

…

[此處省略完整內容以節省空間,但翻譯應涵蓋原始 SKILL.md 的所有部分]

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.

相關技能