在 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% 的场景应使用#{},仅在必要时(如分表)谨慎使用${}。