drizzle-orm-pg 0.13.1 解析:node-postgres 命名预编译语句与 `.prepare(name)` 的使用指南
2026/9/19 22:24:51 网站建设 项目流程

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 addingnameargument to.prepare()method."(通过给.prepare()方法增加name参数,实现 node-pg 预编译语句的使用);
  • 0.13.2:紧随其后的修复版本——"Fix prepared statements usage"(修复预编译语句的使用问题)。

可见 0.13.1 引入的是一条全新的能力线,随后版本对其进行了缺陷修正,说明该特性在当时是持续演进的重点。

什么是命名预编译语句,为什么需要它

PostgreSQL 支持服务端预编译语句(server-side prepared statement):数据库先把 SQL 文本解析、规划(plan)并缓存执行计划,后续执行时只需按名称引用该语句并传入参数值,从而省去重复的解析与规划开销。它在两个层面带来收益:

  1. 性能:对高频执行的同构查询,服务端可复用执行计划,减少解析与规划成本;
  2. 安全:参数与 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.tsPgSelect.prepare(name)(L1107);
  • insert.tsPgInsert.prepare(name)(L415);
  • update.tsPgUpdate.prepare(name)(L591);
  • delete.tsPgDelete.prepare(name)(L257);
  • refresh-materialized-view.tsprepare(name)(L88);
  • query.tsprepare(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)时,传入的nameundefined,即退化为无名查询。

第三层: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: QueryArrayConfigrowMode: '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时还可以注意到两个与预编译执行配套的细节:

  1. 自定义类型解析器:两份QueryConfig都注入了自定义types.getTypeParser(L49-L84 与 L93-L128),对TIMESTAMPTZTIMESTAMPDATEINTERVAL以及对应数组类型(numeric[]、timestamp[]、timestamptz[]、interval[]、date[])返回原始字符串,交由 Drizzle 自己的mapResultRow做行映射(L165-L169),保证 Drizzle 层类型与 pg 原生解析不冲突。

  2. 占位符填充:每次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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询