Communitygithub.com

prisma-patterns

Prisma ORM 用于 TypeScript 后端的模式——模式设计、查询优化、事务、分页以及关键陷阱,如 updateMany 返回计数而非记录、$transaction 超时、migrate dev 重置数据库、@updatedAt 在批量写入中跳过以及无服务器连接耗尽。在编写 Prisma 模式或查询、调试事务、迁移或无服务器连接限制时使用。

prisma-patterns 是什么?

该技能是一个全面的指南,涵盖了在使用 Prisma ORM 构建 TypeScript 后端时的生产级模式和非显而易见陷阱。它通过结构化的表、代码示例和明确的规则,指导开发者进行模式设计(包括 ID 策略和索引)、查询优化(例如 `include` 与 `select` 的对比)、事务选择(数组形式、交互形式)以及分页(推荐使用游标分页)。它还专门针对无服务器环境(如 Vercel 和 AWS Lambda)提供了连接池管理的建议。该技能支持所有 Prisma 版本,并提醒用户检查其版本,因为 API 存在差异(例如 `relationJoins` 和 `$extends` API)。它的核心价值在于通过清晰的“最佳实践”和“反模式”部分,主动预防常见的、代价高昂的错误,例如 `updateMany` 仅返回计数(而不是记录)、`$transaction` 因外部调用而超时、`migrate dev` 的危险使用、`@updatedAt` 在批量更新中失效以及软删除与 `findUniqueOrThrow` 结合时的问题。该技能通过在 `SKILL.md` 源代码中包含的详细反模式示例,帮助开发者避免细微的错误,从而节省数小时的调试时间。

兼容平台~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 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 树索引)
@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 与 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;通过 DATABASE_URL 控制 connection_limit 和 pool_timeout

// 基于适配器的设置(如果您的 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 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 中包含一个唯一约束字段——将 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 dev 仅在本地使用migrate 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/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.

相关技能