必须用 execute() 实现真正参数化:query() 存在字符串插值风险,execute() 强制 PREPARE+EXECUTE,结构与数据隔离;动态表名列名须白名单+正则校验;参数需严格类型匹配;事务内禁用 query()。

必须用 execute(),别碰 query() 带拼接的写法
mysql2 的 query() 方法默认不走服务端预编译,哪怕你传了参数数组,底层仍可能做字符串插值——尤其在含动态表名、列名或特殊字符时,query("SELECT * FROM users WHERE id = ?", [userId]) 看似安全,但若 MySQL 配置宽松(比如 local_infile 开启)、或驱动未设 permitLocalFileAccess: false,攻击者塞个 "1 OR 1=1 UNION SELECT @@version -- " 仍可能触发非预期行为。
execute() 才是真正强制走 PREPARE + EXECUTE 流程:SQL 结构一次性编译,参数纯数据绑定,结构和数据物理隔离。这是 mysql2 里唯一能兑现“参数化”承诺的方法。
- 连接池初始化时显式声明
namedPlaceholders: false(虽然默认是false,但显式写出来防手抖开错) - 所有 SQL 字符串里只允许出现
?占位符,禁止任何模板字符串拼接:`SELECT * FROM ${table}`是红线 - 首次调用同结构
execute()会触发PREPARE,后续复用执行计划,性能略优于反复解析的query()
动态表名/列名只能靠白名单 + 正则校验
预编译机制天生不支持 ? 替换表名、列名、ORDER BY 字段——MySQL 会把 ORDER BY ? 当成字符串字面量处理,查不到数据或直接报 ER_PARSE_ERROR。
正确做法是:白名单硬控 + 字符串拼接(仅限此场景),再加一层正则兜底:
- 定义允许的表名常量:
const allowedTables = ["users", "orders", "products"];,用includes()严格比对 - 列名映射用对象转换:
const sortMap = { created: "created_at", name: "full_name" };,再取sortMap[userInput] || "id" - 拼接前过正则:
/^[a-zA-Z0-9_]+$/,拒绝点号、反引号、空格、分号等一切非标识符字符
参数类型错位会导致查询静默失败
mysql2 对 JS 类型到 SQL 类型的隐式转换非常敏感。比如把布尔值 true 传给 TINYINT(1) 字段没问题,但传给 VARCHAR 就变成字符串 "true",可能匹配不到记录,且无报错——这种“查不到但也不报错”的情况最难排查。
- 明确字段类型,传参前手动转换:
status === "active" ? 1 : 0而不是直接传字符串 - 时间字段优先用
Date实例,避免传字符串格式(如"2026-05-23"可能被当作文本而非日期) - 数值字段避免传
"123"这类字符串,用Number()或一元加号+value强转
事务里混用 query() 和 execute() 很危险
同一个事务中,如果部分语句用 query()(字符串拼接),部分用 execute()(预编译),一旦拼接部分出错,整个事务状态可能异常,且错误堆栈难定位——因为 query() 的 SQL 是运行时拼出来的,而 execute() 的是预编译模板。
- 事务内所有数据库操作统一用
execute(),杜绝混合模式 - 事务外的简单读取(如配置查询)也建议统一风格,避免团队成员无意中引入拼接写法
- CI 阶段加 ESLint 规则,禁用
query(后紧跟模板字符串或变量拼接的模式
最易被忽略的是动态字段那块:很多人以为只要用了 execute() 就万事大吉,结果在 ORDER BY 或 WHERE table_name = ? 上栽跟头。白名单不是可选项,是必选项;正则校验不是锦上添花,是最后一道闸门。


















