客户端扩展总览:$extends 与四种组件
本教程共 54 篇 · 第 35 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:认识客户端扩展(Client Extensions)的四种组件,学会用 $extends 给 Prisma Client 加功能。
Prisma Client 是生成代码,直接改它,下次生成就被覆盖。日志、权限、软删除这类横切逻辑不属于任何模型,写在哪都别扭,重复抄又难维护。客户端扩展(Client Extensions)是官方答案:不动生成代码,在原客户端外面包一层,加方法、改查询、算字段。
v7 中旧的 middleware($use())已经移除,客户端扩展是官方替代。middleware 类型不安全、顺序靠注册先后、调试困难;扩展类型安全、可组合,跑在客户端内部,性能更好。从 v6 升上来时,所有 $use() 代码都要改写成扩展。
四种组件
扩展由一种或多种组件组成,各管一摊:
| 组件 | 作用 | 典型场景 |
|---|---|---|
| model | 给模型加自定义方法 | signUp、exists、findByEmail |
| client | 给客户端加顶层方法 | $log、$totalQueries |
| query | 拦截查询,改参数或结果 | 软删除、审计、权限过滤 |
| result | 给查询结果加计算字段 | fullName、密码脱敏 |
最小的例子,用 model 组件给 user 加一个 signUp 方法:
import { PrismaClient } from "./generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const prisma = new PrismaClient({ adapter }).$extends({
model: {
user: {
async signUp(email: string) {
return prisma.user.create({ data: { email } });
},
},
},
});
await prisma.user.signUp("alice@example.com");
$extends 返回的是扩展客户端(extended client)。原客户端没有任何变化,原有 API 全部保留,新方法叠加在上面。业务方法收拢到模型上,调用方只看到 signUp,不关心里面几步操作。
扩展客户端的三个特性
扩展客户端之间有明确的边界:
- 每个扩展客户端独立运行,互不干扰。
- 多个扩展可以套在同一个客户端上。
- 所有扩展客户端与原客户端共享同一个连接池,不会多开连接。
第 3 点很关键:扩展一百个客户端,数据库连接数不变。共享连接池让「每个请求一个客户端」成为可能。典型用法是实现用户隔离:每个 HTTP 请求构造一个带自己过滤条件的客户端,A 用户永远查不到 B 用户的数据,逻辑收在扩展里,业务代码不用管。也可以按场景造专用客户端:某个请求带调试 cookie 就套一个更啰嗦的日志扩展,其余请求用安静版,互不影响。
扩展可以起名字,报错日志里就能认出是谁:
const prisma = new PrismaClient({ adapter }).$extends({
name: "signUp",
model: { user: { /* ... */ } },
});
多个扩展报错时,日志里的名字能帮你快速定位是哪一段逻辑出了问题。
用 Prisma.defineExtension 组织扩展
直接内联在 $extends 里适合小扩展。要复用、拆文件、打包发布,就用 Prisma.defineExtension 把扩展定义成独立对象:
import { Prisma, PrismaClient } from "./generated/prisma/client";
const auditExtension = Prisma.defineExtension({
name: "audit",
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
console.log(`${model}.${operation}`, JSON.stringify(args));
return query(args);
},
},
},
});
const prisma = new PrismaClient({ adapter }).$extends(auditExtension);
defineExtension 会给扩展作者和使用者都提供严格类型检查与自动补全。query 组件的 $allOperations 能拦下所有模型的全部操作,参数里的 query 是原查询函数,调用 query(args) 继续执行。上面的例子只打印不改动,实际使用中可以在执行前后夹带任意逻辑。
链式组合多个扩展
一个扩展不够用,可以链式叠加:
const prisma = new PrismaClient({ adapter })
.$extends(loggingExtension)
.$extends(auditExtension);
组合后的扩展客户端同时具备两者的功能。多个扩展定义了同名方法时,最后声明的那个生效,也就是「后者优先」。执行顺序上,先声明的扩展先拿到查询,像管道一样层层传递。
两种组合方式按需选:一条链叠到底,适合功能总是成套出现的场景;分开声明多个扩展客户端,各自独立调用,适合按路由、按用户区别对待的场景。
Note链式 $extends 会创建新的扩展客户端,原客户端仍是纯净版。想同时保留两种,就把两条链都存下来,各自调用。
扩展客户端的类型
扩展后的类型可以推导出来。直接定义时用 typeof:
const extendedPrisma = new PrismaClient({ adapter }).$extends({ /* ... */ });
type ExtendedPrisma = typeof extendedPrisma;
单例工厂函数配合 ReturnType:
function getExtendedClient() {
return new PrismaClient({ adapter }).$extends({ /* ... */ });
}
type ExtendedPrisma = ReturnType<typeof getExtendedClient>;
扩展给模型加的计算字段,用 Prisma.Result 类型工具可以精确推导。比如 result 组件给 user 加了 __typename,Prisma.Result<typeof prisma.user, { select: { id: true } }, "findFirstOrThrow"> 就能得到带新字段的完整类型。扩展内部的模型方法想要强类型,还可以用 Exact、Args、Result、Payload 四个工具组合,这一族类型工具是第 37 章的主角。
两个注意事项
第一,扩展客户端上的客户端级方法($connect、$disconnect 等)不一定存在,用前先检查:
if (extendedPrisma.$connect) {
await extendedPrisma.$connect();
}
第二,query 组件不支持嵌套读写操作。它拦截的是顶层查询;create 里的嵌套写入不会逐个触发拦截,想在嵌套写入上做手脚,得在顶层操作里统一处理。
参考来源
- Prisma 官方文档:What are Client Extensions
- Prisma 官方文档:Type utilities
- Prisma 官方文档:Shared Prisma Client extensions
- Mapagam:Working with Client Extensions