关于 prisma-next-runtime
介绍如何在 db.ts 中接入 Prisma Next 数据库运行时,涵盖客户端构建、中间件、环境配置、数据库门面与生命周期管理,适用于应用和短期脚本集成。
主要功能
- 01配置 Postgres、SQLite 和 Mongo 运行时门面
- 02组合遥测、Lint 与预算中间件
- 03区分项目配置和环境变量
- 04管理延迟连接与客户端生命周期
- 05正确使用事务和脚本清理
适用场景
- 01设置应用的 db.ts 入口
- 02为数据库操作配置中间件
- 03运行短期脚本并关闭连接
- 04为数据库门面选择运行时配置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ prisma/ prisma-next --skill prisma-next-runtime安装后在 Claude Code 中输入/prisma-next-runtime确认已加载。
详细介绍
Prisma Next — Runtime(db.ts 配置)
编辑数据契约。其余交给 Prisma 处理。
本技能介绍 运行时入口 — db.ts,以及如何通过扩展、中间件和环境配置来组合数据库客户端。
适用场景
- 用户在初始化后首次配置
db.ts。 - 用户想添加中间件(遥测、lint、预算、自定义中间件)。
- 用户需要按环境进行配置(开发环境与生产环境、多区域)。
- 用户想在 Postgres、SQLite 和 Mongo 外观之间切换。
- 用户想用
db.transaction(...)包装操作(Postgres 和 SQLite)。 - 用户运行一次性脚本(
tsx my-script.ts、Node CLI、CI 任务)时,查询完成后进程仍无法退出,或者需要在脚本中进行清理(db.close()、await using)。 - 用户提到:db.ts、postgres()、mongo()、middleware、telemetry、lints、budgets、DATABASE_URL、.env、connection pool、poolOptions、开发环境与生产环境、事务、只读副本、多数据库、脚本无法退出、挂起、db.close、db.end、关闭连接、pool.end、await using。
不适用场景
- 用户想编写查询 →
prisma-next-queries。 - 用户使用 Supabase — 以角色为先的
supabase()工厂、asUser(jwt)/asAnon()/asServiceRole()、JWT 配置、RLS →prisma-next-supabase。 - 用户想编辑契约 →
prisma-next-contract。 - 用户想将 Prisma Next 接入构建工具(Vite 插件、Next.js 等)→
prisma-next-build。 - 用户想调试连接或运行时错误 →
prisma-next-debug。 - 用户想提交 Bug 或功能请求 →
prisma-next-feedback。
核心概念
db.ts是运行时入口。 它会从@prisma-next/<target>外观(@prisma-next/postgres/runtime、@prisma-next/sqlite/runtime或@prisma-next/mongo/runtime)导入运行时工厂、契约产物(contract.json和contract.d.ts中的Contract类型)以及任意中间件,并导出应用其余部分可导入的db值。- 外观的运行时工厂是用户编写的
db.ts唯一需要导入的接口。 每个工厂都是 默认 导出。Postgres:import postgres from '@prisma-next/postgres/runtime';SQLite:import sqlite from '@prisma-next/sqlite/runtime';Mongo:import mongo from '@prisma-next/mongo/runtime'。工厂签名为<Target><Contract>(options)— 一个类型参数(即contract.d.ts中的Contract类型)和一个 options 对象。 - 延迟连接。 工厂不会同步连接数据库。静态查询接口(
db.sql、db.orm)会立即可用;驱动程序 / 连接池会在首次需要运行时的调用中实例化(或在你显式调用await db.connect({ url })时实例化)。因此,在环境变量就绪前加载的模块也可以导入db.ts。 - 中间件按顺序组合。
middleware: [...]数组中的第一个中间件在 最外层 运行 — 它会在操作进入时最先看到操作,并在操作返回时最后看到操作。先运行遥测中间件,预算 / lint 失败就会显示在遥测跨度内。 prisma-next.config.ts与.env的区别。 配置文件(defineConfig({ contract, db, extensions, migrations }))用于定义项目的静态结构:契约路径、已安装的扩展、迁移目录、默认连接字符串。.env用于存放各环境的值(DATABASE_URL、密钥)。配置文件会通过dotenv/config自动读取.env。在配置文件中硬编码DATABASE_URL会泄露凭据,并绕过按环境覆盖的机制。- 构建系统 / 开发服务器集成属于另一项技能。
vite dev自动生成内容的说明见prisma-next-build。运行时部分(本技能)会读取contract.json/contract.d.ts,无论这些文件如何写入磁盘,因此两项技能可以顺畅配合。
工作流 — 基本 db.ts
概念:db.ts 是生成的契约产物(面向目标)与执行查询的运行时之间的衔接处。有三个关键导入——运行时工厂、Contract 类型(用于为静态查询接口提供类型),以及 JSON 产物(以便运行时在构造时验证结构)。
对于 --target postgres,init 会生成类似这样的内容:
// src/prisma/db.ts
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
});
(目前 init 会在 prisma/db.ts 生成该文件——参见 prisma-next-quickstart 中的 TML-2532。标准路径为 src/prisma/db.ts;src/ 其余部分会根据目录层级从 ./prisma/db 或 ../prisma/db 导入。)
需要了解三点:
<Contract>类型参数至关重要。 如果没有它,静态接口会退化为通用形状,并且模型名称的自动补全也会消失。务必从生成的./contract.d.ts导入Contract。- 必须使用
with { type: 'json' }。 这是 Node 的 ESM JSON 导入属性规范。没有它,导入会报错。 - 构造时的
url是可选的。 如果加载db.ts时没有设置DATABASE_URL,工厂仍会返回客户端;之后可以调用await db.connect({ url })。工厂采用延迟抛错的方式——仅在实际需要运行时时才会抛错。
Mongo façade 的构造形式相同——import mongo from '@prisma-next/mongo/runtime'——生命周期方法也相同,即 db.connect(...) / db.close()。Mongo façade 不提供 db.transaction(...)。 变通方法请参见 Prisma Next 目前尚未实现的功能。ORM 接口有一处不同:键。 在 Mongo 中,db.orm 使用集合的存储名称作为键(取自 @@map(...),如果未设置 @@map,则使用小写的模型名称),而不是 PSL 模型名称——因此,model User { … @@map("users") } 对应 db.orm.users,而不是 db.orm.User。Mongo 完全没有 SQL 构建器接口(db.sql.<table>)(db.sql 的值为 undefined)。完整规则以及 SQL 目标示例的改写方法,请参见 prisma-next-queries § MongoDB ORM 寻址。
工作流 — 作为脚本运行(清理)
概念:连接、查询,然后期望进程退出的短脚本在 Postgres 上会一直挂起,因为 façade 所拥有的 pg.Pool 会让 Node 的事件循环保持运行。数据往返操作会成功,但脚本永远不会退出。请在脚本返回前调用 await db.close()(或者在 脚本模块的顶层使用 await using,以便模块退出时执行清理——关于原因,请参见下方的块作用域警告)。
普通写法 — 从 db.ts 导出 db,在脚本中导入,并在末尾关闭:
// src/scripts/hello.ts
import { db } from '../prisma/db';
const created = await db.orm.User.create({ email: '[email protected]', name: 'Alice' });
const read = await db.orm.User.first();
console.log({ created, read });
await db.close();
TS 5.2+ 惯用写法 — 在脚本模块的顶层构造客户端,并让 [Symbol.asyncDispose] 在模块退出时调用 close():
// src/scripts/hello.ts — top-level await in a script module
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from '../prisma/contract.d';
import contractJson from '../prisma/contract.json' with { type: 'json' };
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
const user = await db.orm.User.first();
console.log(user);
// db.close() runs automatically when the script module exits.
await using 的作用域限定于代码块 — 请勿将其放在请求处理程序中
这是本节最重要的规则。await using db = postgres(...) 会在所属代码块退出时释放资源。在脚本模块中,该代码块是模块主体,资源会在进程退出时释放——这没有问题。在请求处理程序中,所属代码块是处理程序函数,因此资源会在每次请求之后释放——每次调用都会新建一个 pg.Pool,引发 TCP 连接风暴,形成不断拆建连接的热循环。
// DO NOT do this — closes the pool after every request.
app.get('/users', async (req, res) => {
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
const users = await db.orm.User.all();
res.json(users);
});
正确的服务端模式是在 db.ts 中定义模块级单例,由处理程序导入,并且在进程生命周期内绝不关闭:
// src/prisma/db.ts — constructed once, lives for the process
export const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL });
// src/routes/users.ts
import { db } from '../prisma/db';
app.get('/users', async (req, res) => {
const users = await db.orm.User.all();
res.json(users);
});
服务端(HTTP 处理程序、请求循环中的工作线程)在稳定运行期间完全不会调用 db.close()。连接池会在进程生命周期内保持打开。db.close() 和 await using 适用于生命周期较短的脚本——tsx my-script.ts、Node CLI 命令、CI 任务、一次性数据填充运行——不适用于在请求循环中运行的代码。
语义:
close()是幂等的。 调用两次不会产生任何影响。close()是终止性的。 关闭后的db不会重新连接——如果需要另一个连接,请构造一个新客户端。关闭后,db.runtime()、db.connect(...)、db.transaction(...)和db.prepare(...)都会以Error('<target> client is closed')拒绝(例如'Postgres client is closed'、'SQLite client is closed'、'Mongo client is closed')。close()不会中止正在执行的查询。 调用close()前,请先await尚未完成的工作。调用close()后,db.runtime().execute(plan)返回的异步迭代器,以及仍在使用的PreparedStatement句柄,都会在下次调用时失败。- 所有权。
close()只会释放由该门面构造的资源(来自{ url }的pg.Pool、来自{ url }/{ uri, dbName }的MongoClient、来自{ path }的 SQLite 句柄)。如果你提供了自己的pg.Pool/pg.Client(Postgrespg:选项)、mongodb.MongoClient(MongomongoClient:选项)或预构建的binding,db.close()不会触及这些资源——它们的生命周期由你管理。
不存在 db.end()。 通用的 node-postgres 方法名是 pg.Pool 上的 pool.end();Prisma Next 运行时客户端并不是 pg.Pool。正确的调用方式是 await db.close()。
工作流 — 遥测中间件
其理念是:遥测中间件会监视每项操作,并为其生成结构化事件(开始、成功、错误)。将这些事件与可观测性技术栈的收集器配合使用。
import postgres from '@prisma-next/postgres/runtime';
import { createTelemetryMiddleware } from '@prisma-next/middleware-telemetry';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
middleware: [
createTelemetryMiddleware({
onEvent: (event) => {
// forward to your collector, log, etc.
},
}),
],
});
createTelemetryMiddleware 作为独立的用户可安装软件包(@prisma-next/middleware-telemetry)发布,而不是作为 postgres 门面的 /middleware 子路径提供。请直接安装。运行 pnpm ls @prisma-next/middleware-telemetry,确认它已写入锁文件。
工作流 — Lints 和预算中间件
其理念是:lints 会捕获通过类型检查但仍然存在的编写错误(例如,在大型表上执行不带 WHERE 的 DELETE、不带 LIMIT 的 SELECT);预算则会在运行时强制执行行数和延迟上限。两者都会通过结构化错误封装呈现问题,以便 Agent 根据代码分支处理。
这些功能在底层 SQL 运行时软件包(@prisma-next/sql-runtime)中提供,目前尚未从 postgres 门面重新导出——请参阅 Prisma Next 目前尚未实现的功能。examples/prisma-next-demo/src/prisma/db.ts 下的示例应用展示了规范的导入方式。
import postgres from '@prisma-next/postgres/runtime';
import { budgets, lints } from '@prisma-next/sql-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
middleware: [
lints({
severities: {
selectStar: 'warn',
noLimit: 'error',
deleteWithoutWhere: 'error',
updateWithoutWhere: 'error',
readOnlyMutation: 'error',
},
}),
budgets({
maxRows: 10_000,
defaultTableRows: 10_000,
tableRows: { user: 10_000, post: 50_000 },
maxLatencyMs: 1_000,
severities: { rowCount: 'error', latency: 'warn' },
}),
],
});
如需了解完整的选项,请阅读源码:packages/2-sql/5-runtime/src/middleware/lints.ts 和 .../budgets.ts。severities 中的键(lints 的 selectStar、noLimit、deleteWithoutWhere、updateWithoutWhere、readOnlyMutation;budgets 的 rowCount、latency)是唯一可信来源;不要推测 ripgrep 搜不到的键。
工作流 — 组合多个中间件
middleware: [
createTelemetryMiddleware({ onEvent }), // outermost — sees all sub-failures as inner errors
lints({ severities: { noLimit: 'error' } }),
budgets({ maxLatencyMs: 5_000 }), // innermost — runs closest to the driver
],
顺序很重要:最外层会包装内层。遥测放在最前面,预算 / lint 失败就会作为 spans 被记录(Agent 可以在同一条 trace 中,将 lint 代码与操作关联起来)。
工作流 — 配置连接
概念:运行时采用三种绑定形式之一——url、pg(预先构造的 pg.Pool 或 pg.Client),或 binding(显式的类型标签)。三者互斥。pg 形式适用于已自行管理连接池的项目(例如 Lambda 层);url 是默认形式。连接池调优参数为 poolOptions.connectionTimeoutMillis / poolOptions.idleTimeoutMillis,而不是 driverOptions。
// Default — URL string, factory constructs the pool.
postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
poolOptions: {
connectionTimeoutMillis: 20_000,
idleTimeoutMillis: 30_000,
},
});
// BYO pool — pass a pg.Pool you already created.
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env['DATABASE_URL'] });
postgres<Contract>({ contractJson, pg: pool });
url 和 pg 键在类型层面互斥;同时传入两者会报错。
DATABASE_URL 存放在 .env 中。CLI 会在 emit / verify / migration 命令中读取它;运行时会在加载 db.ts 时通过 process.env 读取它。
工作流 — 按环境配置(开发环境与生产环境)
概念:每个环境使用一个 DATABASE_URL;其余 db.ts 结构保持一致。对于中间件差异(例如仅在开发环境中启用严格 lint),可在 db.ts 中根据 process.env['NODE_ENV'] 分支处理。
const isProd = process.env['NODE_ENV'] === 'production';
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL'],
middleware: isProd
? [createTelemetryMiddleware({ onEvent })]
: [
createTelemetryMiddleware({ onEvent }),
lints({ severities: { noLimit: 'error', deleteWithoutWhere: 'error' } }),
],
});
本地使用 .env;生产环境使用部署平台的机密。切勿提交 .env。
工作流 — 事务
此概念适用于 Postgres 和 SQLite。db.transaction(fn) 会开启一个事务,并向回调提供 tx 上下文,其中包含与 db 相同的 sql / orm 接口;回调成功返回时提交,抛出任何错误时回滚。在回调内部,请使用 tx.sql 和 tx.orm,而不是 db.sql / db.orm,以便写入操作在事务中执行。Mongo façade 不提供 db.transaction(...)。
await db.transaction(async (tx) => {
const user = await tx.orm.User.create({ email: '[email protected]' });
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
// If either call throws, both inserts roll back.
});
回调会返回你从中返回的任何值——事务包装器会将其原样传递。tx 对象提供 execute(plan),用于在事务中执行 SQL 构建器计划。
工作流 — 在 Postgres、SQLite 和 Mongo 之间切换
概念:façade 的选择固化在 db.ts(@prisma-next/postgres、@prisma-next/sqlite 或 @prisma-next/mongo)和 prisma-next.config.ts(你从中导入哪个 defineConfig)中。若要切换项目的目标,请在同一目录中重新运行 prisma-next init 并选择其他目标——初始化流程会检测到现有脚手架,并提示是否重新初始化(--force 可跳过提示)。PN 会为新的 façade 重新生成 prisma-next.config.ts 和 db.ts。必须根据新目标的惯用方式重新编写契约源文件(Mongo 表达嵌套文档;Postgres / SQLite 表达关系)。
切换后(Mongo):
// src/prisma/db.ts (Mongo)
import mongo from '@prisma-next/mongo/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = mongo<Contract>({ contractJson, url: process.env['DATABASE_URL'] });
SQLite:
// src/prisma/db.ts (SQLite)
import sqlite from '@prisma-next/sqlite/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = sqlite<Contract>({ contractJson, path: 'app.db' });
构造时 path 为可选参数(之后可调用 db.connect({ path }));省略它时,façade 仍会返回一个客户端。SQLite façade 提供与 Postgres 相同的 db.sql、db.orm、db.transaction(...)、db.close() 和 [Symbol.asyncDispose] 接口。Mongo façade 提供 db.orm、db.close() 和 [Symbol.asyncDispose],但没有 db.sql 和 db.transaction(...)。
db.sql / db.orm 接口的名称保持不变;各接口提供的运算符则根据目标类型而定(Mongo 不支持 JOIN)。
工作流 — 构建系统 / 开发服务器集成
如果你希望开发服务器运行期间自动重新生成契约制品(而不是每次契约源文件变更时都手动运行 prisma-next contract emit),可使用 prisma-next-build 中的构建工具插件:
- Vite:安装
@prisma-next/vite-plugin-contract-emit,并在vite.config.ts中注册prismaVitePlugin('prisma-next.config.ts')。 - Next.js、Webpack、esbuild、Rollup、Turbopack:目前还没有第一方插件——可通过运行
prisma-next contract emit的prebuild脚本作为变通方案。操作指南请参阅prisma-next-build。
运行时部分(本技能)无论如何都相同:db.ts 会从磁盘读取 contract.json + contract.d.ts。构建系统插件的作用是在开发期间确保这些文件保持最新。
常见陷阱
- 在
prisma-next.config.ts中硬编码DATABASE_URL。 这会泄露凭据,并绕过各环境的覆盖设置。请使用.env。 - 在
postgres<Contract>(...)中省略<Contract>类型参数。 如果省略,静态接口会退化为通用形态,你也会失去模型的自动补全功能。没有第二个类型参数——旧的双参数签名(postgres<Contract, TypeMaps>)已不存在。 - 导入合同时忘记添加
with { type: 'json' }。 这是 Node ESM JSON 导入属性规范的要求。 - 中间件顺序很重要。 外层中间件会包装内层中间件。如果希望遥测捕获内层中间件的错误,请将遥测放在最前面。
- 从不存在的门面子路径导入中间件。
@prisma-next/postgres/middleware并不存在。遥测来自@prisma-next/middleware-telemetry;目前,lint / 预算来自@prisma-next/sql-runtime(参见 Prisma Next 尚未实现的功能)。 - 臆造 lint / 预算选项名称。 Lint 使用
severities(包含上述五个键),而不是requireWhere/maxRowsWithoutLimit。预算使用maxLatencyMs(而不是maxDurationMs),以及maxRows/defaultTableRows/tableRows。拿不准时,请阅读源码。 - 切换目标后未重新生成。 合同产物具有目标专属结构;更改目标后请重新生成。
- Postgres 查询结束后脚本仍不退出。
pg.Pool会让 Node 事件循环保持运行。解决方法:在脚本返回前执行await db.close(),或者在脚本模块顶部使用await using db = postgres<Contract>(...)。不要在请求处理程序中使用await using db = postgres(...)——它的作用域仅限于代码块,每次请求后都会关闭连接池。正确的服务端模式是在db.ts中使用模块级单例,使其在整个进程生命周期内保持运行。
Prisma Next 尚未实现的功能
@prisma-next/postgres/middleware子路径。 postgres 门面会重新导出运行时工厂(./runtime)、配置(./config)、合同构建器(./contract-builder)、控制(./control)、系列(./family)、目标(./target)和无服务器(./serverless)——但不包括中间件。目前的变通方法:从@prisma-next/sql-runtime导入lints和budgets,并从@prisma-next/middleware-telemetry导入createTelemetryMiddleware。请通过prisma-next-feedback提交你遇到的其他缺口。- 多数据库路由 / 只读副本。 Prisma Next 未提供内置的主库 / 副本路由器,也没有支持分片的客户端。变通方法:为每个数据存储配置独立的
db.ts实例,并在应用代码中调用正确的实例。如果需要一流的多数据库路由支持,请通过prisma-next-feedback技能提交功能请求。 - 将连接池作为一等配置字段。
poolOptions.connectionTimeoutMillis和poolOptions.idleTimeoutMillis已接入,但pg.Pool的其他调优选项(最大连接数、allowExitOnIdle、SSL 选项等)没有以名称形式公开。变通方法:自行构建pg.Pool并通过pg:传入。如果希望门面公开更多连接池字段,请通过prisma-next-feedback技能提交功能请求。 - 内置查询日志中间件。 Prisma Next 未提供“记录每条查询”的中间件。变通方法:编写一个包装每项操作并记录日志的小型自定义中间件;或者使用
createTelemetryMiddleware,在onEvent回调中记录日志。如果需要内置查询日志,请通过prisma-next-feedback技能提交功能请求。
参考文件
此技能有意仅包含正文;选项级别的详细信息应以 prisma-next init --help、packages/3-extensions/postgres/src/config/define-config.ts 中的 defineConfig 工厂、packages/3-extensions/postgres/src/runtime/postgres.ts 中的 postgres() 工厂,以及 packages/2-sql/5-runtime/src/middleware/{lints,budgets}.ts 中的中间件源码为准。拿不准时,请阅读源码。
检查清单
-
db.ts从@prisma-next/<target>/runtime(postgres、sqlite或mongo)导入运行时工厂,并从./contract.d导入Contract类型。 - 在契约 JSON 导入语句中使用
with { type: 'json' }。 -
<Contract>是postgres<Contract>(...)唯一的类型参数(没有第二个参数)。 -
DATABASE_URL位于.env中,而不是prisma-next.config.ts中。 - 有意安排中间件顺序(通常将遥测放在最外层)。
-
lints/budgets使用已验证的选项键(severities、maxLatencyMs、maxRows、tableRows)。 - 按
NODE_ENV或类似变量控制各环境间的差异(如有)。 - 未在任何已提交的文件中硬编码凭据。
- 未臆造
@prisma-next/postgres/middleware子路径、@prisma-next/postgres-extension-audit包,或postgres<...>的第二个类型参数。 - 未声称 Mongo 门面上存在
db.transaction(...)——只有 Postgres 和 SQLite 提供此功能。 - 未臆造只读副本 / 多数据库 / 额外的连接池配置——已指向 What Prisma Next doesn't do yet,并引导至
prisma-next-feedback。 - 对于构建系统 / 开发服务器相关提示(Vite 插件、Next.js 插件等),已引导至
prisma-next-build。