JSON 转 SQL:PostgreSQL 参数化查询与字面量转义边界
把 JSON 写入数据库和生成一份离线 INSERT 文本,是两个不同的任务。应用程序执行 SQL 时,应让驱动绑定参数;只有确实需要导出文本时,才需要按指定方言生成字面量。本文以 PostgreSQL 为例,专门讨论单引号、NULL、布尔值和有限数值,不讨论嵌套 JSON 展平或数据库建表推断。
一、问题概述:字符串拼接为什么会破坏 SQL
JSON 字符串可以包含单引号,例如 O'Reilly。把它直接插入 SQL 单引号包裹的文本中,会提前结束字面量,后续字符就不再属于值。若这段 SQL 来自用户输入并被执行,字符串拼接还会让输入影响 SQL 语法;这不是 JSON.parse 的问题,而是 SQL 代码与数据没有分离。
二、最小复现:PostgreSQL 中直接拼接不是参数化
下面的字符串仅用于展示错误边界,不应发送给数据库。PostgreSQL 参数占位符使用 $1、$2,值数组由驱动负责编码。
const user = { name: "O'Reilly", active: true };
const unsafe = "INSERT INTO users (name, active) VALUES ('" + user.name + "', " + user.active + ")";
console.log(unsafe);
// INSERT INTO users (name, active) VALUES ('O'Reilly', true)
const text = "INSERT INTO users (name, active) VALUES ($1, $2)";
const values = [user.name, user.active];
// PostgreSQL driver: await client.query(text, values)三、根因:SQL 语法和数据值必须由不同层处理
参数化查询把 SQL 模板作为代码,把 values 作为数据,驱动会按 PostgreSQL 协议传输。单纯做字符串字面量的引号替换,只能讨论某一方言的导出规则,不能处理标识符、关键字、类型转换或动态 SQL 结构,也不能替代参数绑定。
四、推荐方案:写库使用参数化,导出才生成字面量
在线写库时固定 SQL 结构并绑定参数;不要让 JSON 的键名成为未经白名单验证的列名。离线导出可使用受限的 PostgreSQL literal encoder,并明确导出的文件只能交给相同方言的解析器。若需求包含动态列、表名或排序字段,应使用白名单映射,而不是对名称做字符串转义。
五、完整代码:参数化写库与 PostgreSQL 离线字面量
以下 TypeScript 的 runQuery 是驱动适配函数,示例不伪造数据库连接。toPgLiteral 只用于生成离线文本;它拒绝非有限数值和反斜杠文本,遇到这类内容应交给驱动或明确配置过的导出工具。
type Query = (text: string, values: readonly unknown[]) => Promise<unknown>;
async function insertUser(query: Query, user: { name: string; active: boolean | null }) {
const text = "INSERT INTO users (name, active) VALUES ($1, $2)";
return query(text, [user.name, user.active]);
}
function toPgLiteral(value: unknown): string {
if (value === null) return "NULL";
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new TypeError("PostgreSQL numeric literal must be finite");
return String(value);
}
const text = typeof value === "string" ? value : JSON.stringify(value);
if (text === undefined) throw new TypeError("Value is not JSON-serializable");
if (text.includes("\\")) throw new TypeError("Backslash requires explicit PostgreSQL literal settings");
return "'" + text.replaceAll("'", "''") + "'";
}
console.log(toPgLiteral("O'Reilly")); // 'O''Reilly'
console.log(toPgLiteral(null)); // NULL
// Use toPgLiteral only for an offline PostgreSQL script, never as a replacement for query parameters.六、常见错误方案
把所有值都包在单引号中会把 NULL 变成字符串 NULL,也会改变布尔值和数值的语义。只替换单引号并声称支持 MySQL、PostgreSQL 和 SQLite,会忽略各方言的反斜杠、扩展语法和参数协议。把用户提供的列名直接拼入 SQL,即使值已参数化,仍可能改变 SQL 结构。
七、边界条件:undefined、JSON 值与方言
JSON 没有 undefined;输入转换层应先决定缺失字段是省略列还是写入 NULL。NaN 和 Infinity 不是标准 JSON 数字,也不应悄悄写成字符串。数组/对象写入 PostgreSQL jsonb 需要明确列类型和编码策略;本文的离线函数只保证生成一个字符串字面量,不保证目标列类型。
八、如何验证安全边界
先用参数化接口测试包含单引号、反斜杠、换行、NULL、布尔值和 Unicode 的输入,确认数据库收到的值与输入一致。再对离线文本做字符串级断言:O'Reilly 应出现 O''Reilly,NULL 不应出现引号。测试必须连接明确的 PostgreSQL 方言,不能把一份导出文本拿到另一种数据库执行。
九、FAQ
问:把单引号替换成两个单引号能防注入吗?答:它只属于特定 SQL 方言的字面量规则,应用写库仍应使用参数化。
问:参数能绑定表名吗?答:通常不能;表名和列名要用固定白名单映射后拼入 SQL。
问:为什么 null 和 'null' 不一样?答:前者是 SQL NULL,后者是四个字符组成的字符串。
十、总结
PostgreSQL 应用写库的首选是固定 SQL 加 $1 参数绑定。单引号双写只适合明确方言下的离线字面量导出,不能处理动态标识符,也不能替代参数化。验证时覆盖引号、NULL、布尔值、数值和 Unicode,并确认导入目标确实使用同一方言。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
- PostgreSQL Documentation — SQL Syntax
PostgreSQL Global Development Group
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
RFC Editor
相关文章
JSON 转 SQL 类型推断:MySQL 与 PostgreSQL 的边界
分开说明 MySQL 与 PostgreSQL 对 JSON、数字、字符串、数组、对象和 NULL 的类型建议,解释为什么样本推断不能替代明确的数据库 schema。
实现原理嵌套 JSON 展平为关系表:数组、主键与外键如何映射
从一个嵌套订单 JSON 设计客户、订单和明细表,解释一对一与一对多拆分、主外键、空数组和稳定 ID,避免把展平误当成简单改名。
踩坑避坑JSON 大整数精度丢失:Number 安全范围与解决方案
说明 JSON 大整数进入 JavaScript 后为什么会被舍入,演示 Number.MAX_SAFE_INTEGER 边界,并给出字符串契约、BigInt 转换和序列化的可验证方案。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具