MyBatis:#{}与${}区别
#{}是预编译参数,安全防注入,用于传递值。${}是字符串替换,有SQL注入风险,用于动态传入表名、列名等SQL关键字。
这是一个MyBatis中非常核心且重要的问题。简单来说,#{} 和 ${} 的主要区别在于安全性、处理方式和使用场景。
下面我将从这几个方面详细解释它们的区别。
核心总结
| 特性 | #{} (预编译参数) |
${} (字符串替换) |
|---|---|---|
| 工作机制 | 使用JDBC的 PreparedStatement |
简单的字符串拼接/替换 |
| 安全性 | 安全,能有效防止SQL注入 | 不安全,有SQL注入风险 |
| 类型处理 | 自动进行数据类型转换 | 不进行类型处理,直接是文本替换 |
| 性能 | 数据库可预编译和缓存执行计划,性能稍高 | 每次都是新的SQL,数据库需要重新解析 |
| 使用场景 | 传递参数值,如 WHERE 条件、UPDATE 的 SET 值等 |
动态指定SQL关键字或标识符,如表名、列名、ORDER BY 等 |
1. #{} (预编译参数 - Parameter Placeholder)
#{} 是MyBatis中最常用、也是最推荐的方式。
工作原理
它在处理时,会将SQL中的 #{} 替换为 ? 占位符,然后通过JDBC的 PreparedStatement 来设置参数。
示例:
Mapper XML:
xml<select id="getUserById" resultType="com.example.User"> SELECT * FROM users WHERE id = #{userId} </select>当传入
userId = 10时,MyBatis执行的流程如下:- 预编译SQL: 将SQL语句发送到数据库进行预编译:sql
SELECT * FROM users WHERE id = ? - 设置参数: 调用
PreparedStatement的set方法(如setInt,setString)来设置参数?的值为10。
- 预编译SQL: 将SQL语句发送到数据库进行预编译:
优点
防止SQL注入 (最重要!)
因为参数值是作为独立的数据传递给数据库的,而不是作为SQL语句的一部分。数据库不会将参数值解释为SQL命令。
例如,如果一个恶意用户传入userId = "10 OR 1=1",最终设置到?里的值就是字符串"10 OR 1=1",数据库会把它当成一个普通的字符串去匹配id,而不会执行OR 1=1这个逻辑。类型安全
MyBatis会根据参数的Java类型自动转换为对应的JDBC类型,无需手动处理。性能
数据库可以对预编译的SQL语句进行缓存和优化,当相同的SQL语句(只是参数不同)再次执行时,可以重用执行计划,提高性能。
2. ${} (字符串替换 - String Substitution)
${} 的工作方式非常直接:它就是简单的字符串替换。
工作原理
在SQL语句发送到数据库之前,MyBatis会用 ${} 中的变量值直接替换掉 ${} 本身,形成一个完整的SQL字符串。
示例:
Mapper XML:
xml<select id="getUsersByOrder" resultType="com.example.User"> SELECT * FROM users ORDER BY ${columnName} </select>当传入
columnName = "create_time"时,MyBatis生成的最终SQL是:sqlSELECT * FROM users ORDER BY create_time这个完整的SQL字符串会被发送到数据库执行。
缺点和风险
SQL注入风险 (非常危险!)
这是${}最大的问题。因为它只是简单的字符串拼接,如果传入的值来自用户输入,而没有经过严格的校验,就会导致SQL注入。SQL注入攻击示例:
假设有这样一个查询:xml<select id="getUserByName" resultType="com.example.User"> SELECT * FROM users WHERE name = '${userName}' </select>如果一个恶意用户传入
userName的值为:' or '1'='1。
那么拼接后的SQL将变为:sqlSELECT * FROM users WHERE name = '' or '1'='1'这个SQL的
WHERE条件永远为真,将返回users表中的所有数据,造成数据泄露。
合理的使用场景
尽管 ${} 很危险,但在某些特定场景下,它又是必须的,因为 PreparedStatement 的参数占位符 ? 只能用来替换值,不能用来替换SQL的结构性部分,如:
动态指定表名或库名
xml<select id="getDataFromTable" resultType="map"> SELECT * FROM ${tableName} </select>动态指定排序列 (
ORDER BY)xml<select id="getUsers" resultType="com.example.User"> SELECT * FROM users ORDER BY ${orderByColumn} ${sortDirection} </select> <!-- 调用时:getUsers("id", "DESC") -->动态指定要查询的列
xml<select id="getColumns" resultType="map"> SELECT ${columnList} FROM users </select> <!-- 调用时:getColumns("id, name, email") -->
使用 ${} 的安全须知:
当必须使用 ${} 时,永远不要直接使用前端或用户传入的原始值。你必须在后端对这些值进行严格的白名单校验,确保它们是你期望的、安全的标识符(如表名、列名)。
总结与最佳实践
首选
#{}:在任何可以传递参数值的地方(如WHERE条件、INSERT的VALUES、UPDATE的SET),都应该强制使用#{}。这是MyBatis开发的黄金法则。慎用
${}:只在需要动态改变SQL结构(如表名、列名)时才使用${}。校验
${}的输入:当使用${}时,传入的参数必须来自后端代码的控制,或者对用户输入进行了严格的白名单验证,确保其不是恶意的SQL代码片段。
记住这个简单的口诀:“能用 # 就用 #,$ 只用在SQL关键字上,且必须校验”。