首页 / Prisma ORM 入门教程 / 客户端扩展总览:$extends 与四种组件

Prisma ORM 入门教程

客户端扩展总览:$extends 与四种组件

本教程共 54 篇 · 第 35 篇 · 更新于 2026-08-11 · 约 5 分钟阅读

客户端扩展Client Extensions$extendsdefineExtensionmiddlewarequery 组件类型安全

本节目标:认识客户端扩展(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,不关心里面几步操作。

扩展客户端的三个特性

扩展客户端之间有明确的边界:

  1. 每个扩展客户端独立运行,互不干扰。
  2. 多个扩展可以套在同一个客户端上。
  3. 所有扩展客户端与原客户端共享同一个连接池,不会多开连接。

第 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