MyBatis 中 #{ } 与 ${ } 的区别与使用场景
MyBatis 中 #{ } 与 ${ } 的区别与使用场景
1. #{}
的机制与作用
#{}
在 MyBatis 中不是简单的字符串拼接,而是 预编译参数绑定(相当于 JDBC 的 ?
占位符)。
示例
select * from table where name = #{name}
执行时会被 MyBatis 转换为:
select * from table where name = ?
并通过 JDBC 绑定参数:
preparedStatement.setString(1, "小李");
特点
- ✅ 能防止 SQL 注入
- ✅ 自动进行类型转换与转义
- ⚠️ 日志中显示的
'小李'
是日志展示效果,实际执行时并不会在 SQL 字符串中拼接引号
2. ${}
的机制与作用
${}
是 字符串直接替换(文本插入),MyBatis 会将传入值直接拼接到 SQL 中。
示例
order by ${column}
如果 column = name
,实际执行 SQL 为:
select * from table order by name
特点
- ✅ 可用于动态拼接列名、排序方向等 SQL 关键字
- ⚠️ 存在严重的 SQL 注入风险
- ⚠️ 无法自动转义或类型转换
3. 动态排序的场景说明
使用 #{}
:
select * from table order by #{column}
结果(日志显示):
select * from table order by 'name'
数据库会将 'name'
视为字符串常量,而非列名,排序无效。
因此,动态列名或排序字段必须使用 ${}
:
select * from table order by ${column}
4. 安全使用建议
✅ 1. 使用白名单验证
只允许固定列名:
Set<String> allowed = Set.of("name", "age", "created_at");
if (!allowed.contains(column)) {throw new IllegalArgumentException("Invalid column name");
}
然后再安全地使用 ${column}
。
✅ 2. 排序方向白名单
只允许 ASC
/ DESC
:
if (!("ASC".equalsIgnoreCase(order) || "DESC".equalsIgnoreCase(order))) {order = "ASC";
}
✅ 3. 后端映射控制
将前端传参映射为安全列名:
Map<String, String> sortMap = Map.of("name", "user_name","date", "created_at"
);
String column = sortMap.getOrDefault(param, "created_at");
✅ 4. 固定排序逻辑(推荐)
不让前端传列名,而是只传排序选项:
sort=created_desc → ORDER BY created_at DESC
sort=name_asc → ORDER BY user_name ASC
5. 总结对比表
特性 | #{} | ${} |
---|---|---|
类型 | 参数绑定(?) | 字符串替换 |
安全性 | ✅ 防 SQL 注入 | ❌ 高风险 |
是否自动加引号 | ✅(框架处理) | ❌(原样拼接) |
适用场景 | 值参数(条件、更新) | SQL 关键字/列名 |
示例 | where id = #{id} | order by ${column} |
6. 最佳实践结论
- 常规参数 一律使用
#{}
- 动态列名、排序、LIMIT 等 必须使用
${}
,但务必配合白名单或枚举验证 - 切勿将用户输入的内容直接拼接进
${}
,否则可能导致 SQL 注入风险