深入解析MyBatis中#{}和${}的区别与应用场景
在MyBatis框架的使用过程中,SQL映射文件的编写是核心工作之一。而#{}和${}这两种参数占位符语法,虽然看起来相似,却有着本质的区别。正确理解和使用它们,不仅关系到应用程序的安全性,还会影响系统性能。本文将全面剖析这两种语法的区别、实现原理、使用场景以及最佳实践。
一、基本概念与语法
1.1 #{}语法
#{}是MyBatis中的预编译占位符,也称为参数标记。它的基本形式如下:
<select id="findUserById" resultType="User">SELECT * FROM users WHERE id = #{userId}
</select>
1.2 ${}语法
${}是MyBatis中的字符串替换占位符,也称为非转义字符串替换。它的基本形式如下:
<select id="findUsersByTable" resultType="User">SELECT * FROM ${tableName} WHERE status = 1
</select>
二、底层实现原理
2.1 #{}的工作原理
当MyBatis遇到#{}时,会进行以下处理:
解析阶段:MyBatis解析SQL映射文件时,识别出#{}标记
参数处理:运行时将参数值通过PreparedStatement的set方法设置
SQL生成:最终生成带有"?"的预编译SQL语句
例如上面的例子会生成:
SELECT * FROM users WHERE id = ?
然后通过PreparedStatement的setInt/setString等方法设置参数值。
2.2 ${}的工作原理
对于${}的处理则完全不同:
直接替换:MyBatis在SQL解析阶段就直接将${}替换为实际的参数值
字符串拼接:替换后的SQL语句是通过字符串拼接而成的
直接执行:最终生成的是完整的SQL语句,而非预编译语句
例如,如果tableName="users",生成的SQL就是:
SELECT * FROM users WHERE status = 1
三、核心区别对比
3.1 安全性对比
特性 | #{} | ${} |
---|---|---|
SQL注入防护 | 安全,能防止SQL注入 | 不安全,存在SQL注入风险 |
实现方式 | 参数化查询 | 字符串拼接 |
#{}示例:
String sql = "SELECT * FROM users WHERE name = #{name}";
// 即使用户输入 name = "' OR '1'='1"
// 实际执行:SELECT * FROM users WHERE name = ?
// 参数值会被正确处理,不会导致注入
${}示例:
String sql = "SELECT * FROM users WHERE name = '${name}'";
// 如果用户输入 name = "' OR '1'='1"
// 实际执行:SELECT * FROM users WHERE name = '' OR '1'='1'
// 这将返回所有用户数据,造成SQL注入
3.2 性能对比
特性 | #{} | ${} |
---|---|---|
数据库优化 | 支持预编译,可缓存执行计划 | 每次都是新SQL,无法缓存 |
网络传输 | 只需传输参数 | 需要传输完整SQL |
编译次数 | 一次编译多次执行 | 每次都需要重新编译 |
3.3 使用场景对比
场景 | #{} | ${} | 说明 |
---|---|---|---|
普通参数值 | ✓ | ✗ | 推荐使用#{} |
表名 | ✗ | ✓ | 动态表名必须使用${} |
列名 | ✗ | ✓ | 动态列名必须使用${} |
ORDER BY子句 | ✗ | ✓ | 动态排序需谨慎使用 |
GROUP BY子句 | ✗ | ✓ | 动态分组需谨慎使用 |
LIKE模糊查询 | ✓ | ✗ | 需特殊处理通配符 |
四、深入应用场景
4.1 必须使用#{}的场景
所有用户输入的参数值
WHERE username = #{username} AND password = #{password}
数值型参数
WHERE age > #{minAge} AND age < #{maxAge}
日期型参数
WHERE create_time > #{startDate}
4.2 可能需要使用${}的场景
动态表名
SELECT * FROM ${tableName}
适用于分表场景,如表名按年份分表:user_2022, user_2023等
动态列名
SELECT ${columns} FROM users
适用于动态选择返回字段的场景
ORDER BY排序
ORDER BY ${sortColumn} ${sortOrder}
但更安全的做法是:
<choose><when test="sortColumn == 'name'">ORDER BY name</when><when test="sortColumn == 'age'">ORDER BY age</when><otherwise>ORDER BY id</otherwise> </choose>
4.3 特殊场景处理
LIKE模糊查询的正确写法:
错误方式:
WHERE name LIKE '%${name}%'
正确方式:
// Java代码中处理参数
String nameParam = "%" + name + "%";
WHERE name LIKE #{nameParam}
或使用SQL函数:
WHERE name LIKE CONCAT('%', #{name}, '%')
五、最佳实践建议
5.1 安全性实践
默认使用#{}:除非确有必要,否则总是使用#{}
严格过滤${}参数:使用${}时,必须对参数值进行白名单验证
// 验证表名是否合法 if (!isValidTableName(tableName)) {throw new IllegalArgumentException("Invalid table name"); }
避免用户输入直接用于${}:特别是排序、分组等场景
5.2 性能优化实践
优先使用#{}:利用预编译语句的缓存优势
减少${}使用频率:对于频繁调用的SQL,避免使用${}导致无法缓存执行计划
批量处理动态SQL:对于必须使用${}的场景,考虑批量处理减少SQL解析次数
5.3 代码可维护性实践
明确注释:在使用${}的地方添加注释说明原因
<!-- 必须使用${}因为表名是动态的 --> SELECT * FROM ${tableName}
集中管理:将动态部分集中管理,便于维护和安全检查
单元测试:为使用${}的SQL编写额外的安全测试用例
六、常见问题解答
Q1:为什么ORDER BY不能使用#{}?
A:因为#{}会给参数值添加引号,例如:
ORDER BY 'name' 'DESC' -- 错误的SQL语法
而正确的应该是:
ORDER BY name DESC
Q2:什么情况下必须使用${}?
A:当SQL语句的非参数部分需要动态变化时,如:
动态表名
动态列名
SQL关键字(如ASC/DESC)
Q3:如何安全地使用${}?
A:可以采取以下措施:
使用白名单验证参数值
避免直接使用用户输入
对参数值进行转义处理
最小化使用范围
总结
#{}和${}在MyBatis中扮演着不同的角色:
#{} 是安全的参数占位符,适用于几乎所有参数值的场景,能防止SQL注入,性能更好。
${} 是字符串替换,适用于SQL语句本身需要动态变化的场景,但存在安全风险,应当谨慎使用。
在实际开发中,我们应该遵循以下原则:
默认使用#{}
谨慎评估${}的使用必要性
对必须使用${}的场景实施严格的安全控制
编写清晰的文档和注释说明使用原因
正确理解和使用这两种占位符,将使你的MyBatis应用更加安全、高效和可维护。