MyBatis 参数传递:#{} 与 ${} 的完整指南
一、核心结论
- #{} —— 参数占位符(预编译)
用于安全传值,MyBatis 内部通过 PreparedStatement,自动防止 SQL 注入。 - ${} —— 字符串拼接(非预编译)
用于动态拼接 SQL 片段,本质就是字符串替换,安全性极低,必须谨慎使用。
二者不是替代关系,而是互补兄弟:
- #{} 管 值;
- ${} 管 结构。
二、工作机制深度解析
1. #{} 的工作原理
- 替换为占位符
将 SQL 中的 #{} 替换成 ? 占位符。
SELECT * FROM user WHERE name = #{name} AND age = #{age}
最终 SQL:
SELECT * FROM user WHERE name = ? AND age = ?
- 安全赋值
MyBatis 使用 PreparedStatement.setXxx() 为 ? 绑定参数。
- 字符串会自动加引号并转义
- 类型自动处理(数值、日期等)
- 执行
SQL 结构已编译好,可缓存复用,仅需传参即可执行。
优点:安全、性能好、自动处理类型,防止 SQL 注入。
2. ${} 的工作原理
- 原样拼接
MyBatis 将 ${} 内的值直接拼接到 SQL 字符串。
SELECT * FROM ${tableName}
传入 tableName = user_order,最终 SQL:
SELECT * FROM user_order
- 直接执行
SQL 作为完整字符串传递给数据库执行。
缺点:存在严重 SQL 注入风险。
例如:
SELECT * FROM user_order; DROP TABLE user; --
三、应用场景对比
适用 #{} ——传递值
凡是涉及具体数据的地方:
- WHERE 条件:WHERE id = #{id}
- INSERT 数据:VALUES (#{id}, #{name})
- UPDATE 修改:SET name = #{newName}
适用 ${} ——拼接结构
凡是涉及SQL 结构变化,且 #{} 无法实现的地方:
- 动态表名 / 分表查询
SELECT * FROM order_${year}
- 动态排序
ORDER BY ${sortColumn} ${sortDirection}
必须做白名单校验。
- 函数或关键字
SELECT ${aggFunc}(price) FROM order
- IN 子句(常见误区)
错误用法:
WHERE id IN (${ids})
正确用法(foreach + #{}):
WHERE id IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
四、对比总结表
特性 | #{} 参数占位符 | ${} 字符串替换 |
本质 | 预编译(PreparedStatement) | 拼接(Statement) |
安全性 | 极高(防SQL注入) | 极低(高危注入风险) |
性能 | 高(SQL骨架可缓存) | 低(每次重新解析) |
引号处理 | 自动加引号并转义 | 手动加 ' |
典型场景 | WHERE 条件、INSERT、UPDATE 值 | 动态表名、列名、排序、关键字 |
推荐度 | 99% 场景首选 | 少数场景备用,必须校验 |
五、最佳实践与安全规范
- 默认使用 #{} —— 传值就用 #,这是最安全的选择。
- ${} 仅在必要时使用 —— 例如动态表名、列名、排序。
- 强制白名单校验 —— 任何 ${} 参数必须经过严格检查。
List<String> whiteList = Arrays.asList("name", "age", "create_time");
if (!whiteList.contains(sortColumn)) {
sortColumn = "id"; // fallback 默认列
}
- 避免偷懒 —— 不要因为 $ 拼接方便就滥用,它是 SQL 注入的高危入口。
六、总结
- #{}:传递数据,预编译 + 安全 + 高性能 → 默认首选。
- ${}:拼接结构,灵活但高危 → 谨慎使用,必须校验。
记住一句话:
“用 # 传值,用 $ 改结构。”