Communitygithub.com

Prisma Patterns

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

Prisma Patterns 是什么?

这是一个针对 Prisma ORM 在 TypeScript 后端中使用的生产级模式与隐蔽陷阱的实用指南。它涵盖了从 schema 设计(如 ID 策略、索引)到查询优化(如 `include` 与 `select` 的选择)的核心概念,并提供了事务的两种形式(数组形式和交互式形式)及其适用场景。该指南详细解释了 PrismaClient 的单例模式以避免连接池浪费,并重点解决了 N+1 查询问题。在代码示例部分,提供了游标分页、软删除、错误处理以及无服务器环境下的连接池配置等典型实现。反模式部分揭示了几个关键陷阱:`updateMany` 和 `deleteMany` 仅返回计数而不返回记录、`$transaction` 交互式表单的默认 5 秒超时、`migrate dev` 可能导致数据库重置,以及 `@updatedAt` 在 `updateMany` 中不会自动更新。它还涵盖了 `findUniqueOrThrow` 与软删除结合时的漏洞,以及破坏性 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 仅在 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)完全在事务外
// 数组形式 — 在一个往返中批量处理
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 对每个迁移文件进行校验。应用后编辑会导致原始迁移已运行的每个环境出现 P3006 checksum mismatch。请改为创建新的迁移。

破坏性 schema 变更需要多步骤迁移

一次迁移中向现有列添加 NOT NULL 或重命名列会锁定表或丢弃数据。使用扩缩法(expand-and-contract):

# 步骤 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.

相关技能