drizzle-orm-pg 0.13.1 解析:node-postgres 命名预编译语句与.prepare(name)的使用指南
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
本指南围绕 drizzle-orm-pg 0.13.1 的核心变更展开:该版本为node-postgres驱动实现了对 PostgreSQL 服务端预编译语句(prepared statements)的完整支持,具体方式是给查询构建器的.prepare()方法新增name参数。读完本文,你将理解为什么命名预编译语句对性能与安全性有价值、如何在 Drizzle 中为 select/insert/update/delete 等查询命名并复用预编译语句,以及该特性在驱动源码中的真实调用链。需要说明的是,drizzle-orm-pg 0.13.1 是一个历史独立发布版本,其功能在后续版本中已并入主包drizzle-orm,对应实现位于本仓库的 drizzle-orm/src/node-postgres 目录,本文将以该仓库当前源码为事实依据展开。
版本背景:0.13.x 时间线
本仓库的 changelogs/drizzle-orm-pg 目录保留了该历史包逐版本的变更记录:
- 0.13.0:正式发布(
Release 🎉); - 0.13.1:本次核心变更——"Implemented node-pg prepared statements usage via adding
nameargument to.prepare()method."(通过给.prepare()方法增加name参数,实现 node-pg 预编译语句的使用); - 0.13.2:紧随其后的修复版本——"Fix prepared statements usage"(修复预编译语句的使用问题)。
可见 0.13.1 引入的是一条全新的能力线,随后版本对其进行了缺陷修正,说明该特性在当时是持续演进的重点。
什么是命名预编译语句,为什么需要它
PostgreSQL 支持服务端预编译语句(server-side prepared statement):数据库先把 SQL 文本解析、规划(plan)并缓存执行计划,后续执行时只需按名称引用该语句并传入参数值,从而省去重复的解析与规划开销。它在两个层面带来收益:
- 性能:对高频执行的同构查询,服务端可复用执行计划,减少解析与规划成本;
- 安全:参数与 SQL 模板分离,天然规避 SQL 注入风险。
node-postgres(pg)驱动对预编译语句的支持方式是:在查询配置对象(QueryConfig)中提供name字段。当name存在时,驱动通过扩展查询协议在服务端准备并命名该语句,之后的同名调用直接按名执行;当name缺省时,则退化为简单的文本查询或无名(unnamed)预编译语句。0.13.1 之前,Drizzle 的 node-postgres 驱动没有把name暴露给用户,也就无法利用服务端命名预编译语句这一能力;0.13.1 通过给.prepare(name)增加name参数,将这条链路完整打通。
.prepare(name)的公开 API 用法
在 0.13.1 之后,面向用户的用法非常直观:对查询构建器调用prepare(name),传入一个字符串名称,即可得到一个预编译的查询对象,之后可反复调用其execute()(或all())执行,而数据库会按名称识别并复用这条预编译语句。
在 pg-core/query-builders/select.ts 中,官方对prepare的 JSDoc 注释给出了明确语义:
Create a prepared statement for this query. This allows the database to remember this query for the given session and call it by name, rather than specifying the full query.
即:为这条查询创建预编译语句,让数据库在给定会话(session)内记住该查询,并通过名称调用它,而无需每次重复发送完整 SQL。PgSelect的公开方法签名如下:
prepare(name: string): PgSelectPrepare<this> { return this._prepare(name); }同样的prepare(name)模式也存在于 pg-core 下其余查询构建器中,通过检索 pg-core/query-builders 目录可以看到完整的覆盖范围:
select.ts的PgSelect.prepare(name)(L1107);insert.ts的PgInsert.prepare(name)(L415);update.ts的PgUpdate.prepare(name)(L591);delete.ts的PgDelete.prepare(name)(L257);refresh-materialized-view.ts的prepare(name)(L88);query.ts的prepare(name)(L112)。
一个典型的命名预编译查询用法示例:
import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const db = drizzle(pool); // 为按 id 查询用户的语句命名,之后可重复执行 const getUserById = db.select().from(users).where(eq(users.id, sql.placeholder('id'))) .prepare('get_user_by_id'); const user1 = await getUserById.execute({ id: 1 }); const user2 = await getUserById.execute({ id: 2 });execute(placeholderValues)中传入的占位值对象会替换查询中的sql.placeholder(...)参数位(见下文源码中fillPlaceholders的处理),而 SQL 文本本身在整个生命周期内保持不变,数据库可稳定按名称命中已缓存的预编译语句。
源码级调用链:name 是如何透传到 pg 驱动的
理解这条特性的关键,是追踪name从用户调用到驱动执行层的完整传递路径。整条链路横跨三层源码:
第一层:查询构建器(公开 API 层)。以PgSelect为例,prepare(name)调用内部_prepare(name)(select.ts L1078-L1098),其中关键一行是:
const query = session.prepareQuery<PreparedQueryConfig & { execute: TResult }>( dialect.sqlToQuery(this.getSQL()), fieldsList, name, // ← name 在此传入 true, undefined, { type: 'select', tables: [...usedTables] }, cacheConfig, );第二层:PgSession 抽象会话层。在 pg-core/session.ts 中,prepareQuery被定义为抽象方法,其签名中name: string | undefined是标准参数之一。同时注意 L194-L207 的execute快捷路径:当用户直接执行(未显式prepare)时,传入的name为undefined,即退化为无名查询。
第三层:NodePgSession / NodePgPreparedQuery(驱动实现层)。这是 0.13.1 变更真正落地的位置,见 node-postgres/session.ts:
NodePgSession.prepareQuery(L221-L246)把收到的name原样透传给NodePgPreparedQuery构造函数;NodePgPreparedQuery构造函数(L27-L131)将name写入两份 pg 查询配置对象:rawQueryConfig: QueryConfig(文本/对象结果模式),见 L44-L46;queryConfig: QueryArrayConfig(rowMode: 'array'数组结果模式),见 L87-L90;
- 执行时(
execute/all,L133-L188),根据查询形态选择对应配置调用client.query(config, params),name随配置一并进入 pg 驱动,触发服务端命名预编译语句流程。
由此可以确认 0.13.1 的实现方式:Drizzle 并未自行实现预编译逻辑,而是把name这一关键参数完整桥接到 node-postgres 的QueryConfig上,让 pg 驱动原生能力生效。这与变更记录中 "via addingnameargument"(通过增加name参数)的描述完全一致。
实现细节补充:类型解析器与占位符填充
在阅读NodePgPreparedQuery时还可以注意到两个与预编译执行配套的细节:
自定义类型解析器:两份
QueryConfig都注入了自定义types.getTypeParser(L49-L84 与 L93-L128),对TIMESTAMPTZ、TIMESTAMP、DATE、INTERVAL以及对应数组类型(numeric[]、timestamp[]、timestamptz[]、interval[]、date[])返回原始字符串,交由 Drizzle 自己的mapResultRow做行映射(L165-L169),保证 Drizzle 层类型与 pg 原生解析不冲突。占位符填充:每次
execute都会先执行fillPlaceholders(this.params, placeholderValues)(L135),把 Drizzle 的sql.placeholder(...)参数与用户传入的值合并成最终参数数组,再交给 pg 驱动。这与上文示例中execute({ id: 1 })的用法一一对应。
使用注意事项与适用边界
基于 node-postgres 驱动语义和当前源码,使用该特性时有几点需要留意:
- 预编译语句是会话(连接)级的:node-postgres 的命名预编译语句缓存在单个连接上。在
Pool场景下,每次client.query可能命中池中不同连接,同名语句需要在对应连接上重新准备。从源码看,NodePgSession在事务中会区分 Pool 与单连接两种情况(session.ts L252-L266),prepareQuery的 JSDoc 也明确表述为"for the given session"。因此,若追求跨连接完全复用,通常需要配合连接复用策略或在单连接(Client)上使用。 - 名称需可预期且唯一:
name在同一连接内应保持稳定唯一,避免不同 SQL 共用同名导致服务端语句覆盖或冲突。 - 0.13.2 的修复:0.13.1 引入后,0.13.2 随即修复了预编译语句使用中的问题。若参考历史版本,建议关注 0.13.2 及之后的修复版本;在当今统一包
drizzle-orm的 node-postgres 实现中,该链路已趋于稳定。 - 需要预编译之外的选择:若不需要服务端命名预编译(例如一次性查询),直接
await db.select()...即可,execute快捷路径会以name = undefined的无名方式执行(pg-core/session.ts L194-L207),行为与 0.13.1 之前的版本一致,不影响既有代码。
总结
drizzle-orm-pg 0.13.1 通过给.prepare()方法增加name参数,将 node-postgres 的服务端命名预编译语句能力完整接入 Drizzle:用户只需为高频查询命名并复用,即可获得执行计划复用带来的性能收益与参数化查询带来的安全性。从源码看,该能力是 Drizzle 查询构建器层、PgSession抽象层与NodePgPreparedQuery驱动层三层协作的结果,核心机制是把name透传进 pg 驱动的QueryConfig。该特性今日已并入主包drizzle-orm,在 drizzle-orm/src/node-postgres/session.ts 中持续可用,值得在基于 node-postgres 的高频查询场景中优先采用。
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考