MyBatis 中 ${} 和 #{} 有什么区别?
一句话总结
#{}:预编译参数占位符 (?),走PreparedStatement与TypeHandler,防 SQL 注入,用于"值"。${}:字符串原样拼接 (文本替换),不做类型处理与转义,有 SQL 注入风险 ,仅用于"SQL 片段"(表名/列名/排序等),必须配合白名单。
1. 核心区别对比(背这一张表)
| 维度 | #{} |
${} |
|---|---|---|
| 替换方式 | 生成 ? 占位符,参数通过 JDBC 绑定 |
直接把字符串拼进 SQL(文本替换) |
| 底层实现 | PreparedStatement + TypeHandler |
字符串拼接 |
| SQL 注入 | 低(参数不会当 SQL 执行) | 高(输入可变成 SQL) |
| 类型处理 | 有(按 Java/JDBC 类型设置参数) | 无(你给啥拼啥) |
| 适用对象 | 值:where 条件值、insert/update 的值、limit 值等 | SQL 片段:表名、列名、排序字段、排序方向、分区名等 |
| 性能 | 预编译可复用执行计划(通常更好) | SQL 文本变化大,缓存/复用更差 |
2. 底层原理:本质是"参数绑定" vs "文本替换"
2.1 #{}:参数绑定(PreparedStatement)
写法:
xml
<select id="findUser" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
MyBatis 生成的 SQL(形态):
sql
SELECT * FROM user WHERE name = ?
随后由 JDBC 绑定参数:
TypeHandler决定如何把 Java 类型设置到 JDBC(setString/setInt/setTimestamp...)- 参数作为"值"传递,不会被当成 SQL 语法的一部分执行,因此天然抗注入
2.2 ${}:文本替换(拼接到 SQL)
写法:
xml
<select id="findUsers" resultType="User">
SELECT * FROM user ORDER BY ${sortField} ${sortOrder}
</select>
当 sortField=age、sortOrder=DESC 时,直接拼成:
sql
SELECT * FROM user ORDER BY age DESC
这一步发生在 SQL 发送到数据库之前,MyBatis 不会对 ${} 做转义/类型处理。
3. SQL 注入风险:${} 真实危险点
如果用户可控输入被用于 ${}:
sortField = "age desc; drop table user; --"
可能拼成:
sql
SELECT * FROM user ORDER BY age desc; drop table user; -- DESC
数据库是否允许多语句与驱动配置有关,但风险本质存在。
结论:
- 值一律用
#{} ${}必须做白名单/枚举映射或用 MyBatis 动态标签替代
4. 典型使用场景(怎么用才对)
4.1 必须用 #{} 的场景(值)
WHERE id = #{id}SET name = #{name}INSERT INTO ... VALUES (#{v1}, #{v2})LIMIT #{offset}, #{pageSize}(注意不同数据库方言)
4.2 可能用 ${} 的场景(SQL 标识符/片段)
- 动态表名:
FROM ${tableName} - 动态列名:
SELECT ${columnName} FROM ... - 动态排序:
ORDER BY ${sortField} ${sortOrder}
这些场景的共同点:SQL 标识符无法用 ? 占位符替代 ,因为 JDBC 的 ? 只能替换"值",不能替换表名/列名/关键字。
5. 安全写法:用白名单/<choose> 替代 ${}
5.1 推荐:<choose> 显式枚举可排序字段(避免 ${sortField})
xml
<select id="findUsers" resultType="User">
SELECT *
FROM user
<choose>
<when test="sortField == 'age'">ORDER BY age</when>
<when test="sortField == 'name'">ORDER BY name</when>
<otherwise>ORDER BY id</otherwise>
</choose>
<choose>
<when test="sortOrder == 'DESC'">DESC</when>
<otherwise>ASC</otherwise>
</choose>
</select>
特点:
- 没有
${},注入面大幅收敛 - 允许的字段与方向被限制在固定集合
5.2 业务侧白名单映射(仍需谨慎)
在 Java 侧把外部输入映射成安全枚举,再传给 Mapper:
sortField只能从{"age","name","id"}中选sortOrder只能是ASC/DESC
即使最终仍用 ${},也要确保输入不可直接透传。
6. 常见坑(面试/实战都爱问)
6.1 ${} 会不会自动加引号?
不会。${} 就是纯拼接:
WHERE name = ${name}若name=Alice会变成WHERE name = Alice(SQL 语法可能错误)- 若你写成
WHERE name = '${name}'才会带引号,但这会把问题变成"手写拼接字符串",更容易注入
6.2 LIKE 怎么写?
推荐写法(仍用 #{}):
xml
WHERE name LIKE CONCAT('%', #{name}, '%')
不要写:
WHERE name LIKE '%${name}%'(高风险)
6.3 IN 查询怎么写?
- 多值应使用
<foreach>生成多个#{}占位符,而不是把整段字符串塞进${}。
7. 性能与缓存(知道即可)
#{}:SQL 形态更稳定(?占位),数据库更容易复用执行计划;MyBatis 也更容易复用MappedStatement下的预处理流程。${}:SQL 文本随输入变化,可能导致数据库解析/硬解析次数增多(不同数据库表现不同)。
8. 结论(一句话复述)
- 能用
#{}就别用${}。 ${}只能用于"表名/列名/排序"等无法参数绑定 的 SQL 片段,并且必须白名单,否则就是给 SQL 注入开门。