在 MyBatis 中,${} 和 #{} 是两种处理 SQL 参数的占位符,它们在实现机制、安全性、使用场景上存在显著差异。以下是详细对比:
核心区别对比
| 特性 | #{} | 
${} | 
|---|---|---|
| 底层机制 | 预编译占位符(PreparedStatement) | 
字符串直接替换 | 
| 安全性 | ✅ 防止 SQL 注入 | ❌ 存在 SQL 注入风险 | 
| 参数处理 | 自动添加引号(字符串/日期类型) | 需手动添加引号(否则语法错误) | 
| 性能 | 较低(预编译开销) | 较高(直接拼接 SQL) | 
| 适用场景 | 动态参数值(如 WHERE id = ?) | 
动态 SQL 片段(如表名、排序字段) | 
机制详解
- 
#{}(预编译占位符)- 
MyBatis 会将其解析为 JDBC 的?占位符,通过PreparedStatement预编译 SQL。
 - 
参数值会被安全转义,例如:
sqlSELECT * FROM users WHERE name = #{name} -- 转换为:SELECT * FROM users WHERE name = ?若name = "John",实际执行时参数值会被安全绑定为'John'。
 
 - 
 - 
${}(字符串替换)- 
直接替换为参数值的字面量,无预编译或转义。
 - 
例如:
sqlSELECT * FROM users WHERE id = ${id} -- 若 id=1,替换为:SELECT * FROM users WHERE id = 1若id = "1 OR 1=1",则 SQL 会变为:
javaSELECT * FROM users WHERE id = 1 OR 1=1 -- 查询所有数据,存在 SQL 注入风险[1,5](@ref) 
 - 
 
安全性问题
- 
#{}:天然防 SQL 注入,适用于用户输入或外部参数。 - 
${}:高风险!仅适用于完全可控的静态值(如内部生成的表名、列名)。错误示例(模糊查询):
SELECT * FROM products WHERE name LIKE '%${keyword}%' -- 若 keyword="' OR 1=1 --",导致数据泄露! 
使用场景对比
✅ #{} 的适用场景
- 
普通条件查询(值动态传递):
sqlSELECT * FROM orders WHERE user_id = #{userId} [1,4](@ref) - 
日期/字符串参数(自动添加引号):
sqlINSERT INTO logs (content) VALUES (#{logContent}) -- 自动转为 'xxx' [7,8](@ref) - 
模糊查询(安全写法):
sqlSELECT * FROM products WHERE name LIKE CONCAT('%', #{keyword}, '%') [2,3](@ref) 
⚠️ ${} 的适用场景
- 
动态表名/列名(SQL 片段不可预编译):
sqlSELECT * FROM ${tableName} WHERE ${column} = 1 [3,5,7](@ref) - 
排序字段(如ORDER BY ${sortField}):
sqlSELECT * FROM users ORDER BY ${orderBy} DESC [3,7](@ref) - 
批量操作(如IN子句):
sqlDELETE FROM cart WHERE id IN (${ids}) -- ids="1,2,3" [7](@ref) 
最佳实践与避坑指南
- 
默认使用
#{}:除非必须动态拼接 SQL 片段,否则一律用#{}确保安全。 - 
${}的防御措施:- 
仅允许传入白名单值(如预定义表名列表)。
 - 
手动过滤危险字符(如空格、分号)。
 
 - 
 - 
模糊查询的替代方案:
- 
用CONCAT函数(推荐):
SELECT * FROM table WHERE name LIKE CONCAT('%', #{text}, '%') - 
程序层拼接(Java 中生成"%text%"再传入)。
 
 - 
 
💎 总结
- 
#{}= 安全优先 :处理动态值(用户输入、条件参数),预编译防注入。 - 
${}= 谨慎使用 :处理动态 SQL 片段(表名、排序),需严格校验输入。 
关键口诀:**"值用井号(
#),结构用刀($)"------值动态用#{},SQL 结构动态用${}。实际开发中,95% 的场景应使用#{},仅在必要时(如分表)谨慎使用${}。