题目
MyBatis 中如何安全高效地处理动态 IN 查询并防止 SQL 注入?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
动态SQL编写,SQL注入防护,批量查询性能优化
快速回答
在 MyBatis 中处理动态 IN 查询需要解决三个核心问题:
- 动态参数构建:使用
<foreach>标签遍历集合 - SQL 注入防护:必须使用
#{}预编译占位符 - 性能优化:避免超过数据库 IN 条件限制(如 Oracle 的 1000 条限制)
推荐解决方案:
- 使用
<foreach>+#{}构建安全查询 - 通过分批查询解决大集合问题
- 使用
@Param注解明确集合名称
1. 问题背景与挑战
在 MyBatis 中执行类似 SELECT * FROM users WHERE id IN (?) 的查询时,直接传入集合会导致:
- SQL 注入风险:使用
${}拼接参数时恶意输入可能破坏查询 - 性能问题:数据库对 IN 子句的参数数量有限制(如 Oracle 限制 1000 条)
- 语法错误:集合直接映射为
IN (1,2,3)但预编译占位符要求IN (?)
2. 安全解决方案(核心代码)
<select id="selectUsersByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id} <!-- 关键:必须用 #{} 防止注入 -->
</foreach>
</select>对应 Mapper 接口:
List<User> selectUsersByIds(@Param("ids") List<Long> ids);3. 性能优化策略
3.1 分批查询(解决大集合问题)
// 服务层实现分批
public List<User> batchSelectUsers(List<Long> ids) {
List<User> result = new ArrayList<>();
// 每批 500 个 ID
List<List<Long>> batches = Lists.partition(ids, 500);
for (List<Long> batch : batches) {
result.addAll(userMapper.selectUsersByIds(batch));
}
return result;
}3.2 临时表方案(超大数据集)
<!-- 1. 创建临时表 -->
<insert id="createTempTable">
CREATE TEMP TABLE temp_ids (id BIGINT)
</insert>
<!-- 2. 批量插入 -->
<insert id="batchInsertIds">
INSERT INTO temp_ids VALUES
<foreach item="id" collection="ids" separator=",">
(#{id})
</foreach>
</insert>
<!-- 3. JOIN 查询 -->
<select id="selectByTempTable" resultType="User">
SELECT u.* FROM users u
JOIN temp_ids t ON u.id = t.id
</select>4. 常见错误与防护
| 错误做法 | 风险 | 正确方案 |
|---|---|---|
WHERE id IN (${ids}) |
高危 SQL 注入 | 使用 #{} + <foreach> |
| 单次传入超 1000 个 ID | 数据库报错(ORA-01795) | 分批查询或临时表 |
忘记 @Param("ids") |
抛出 BindingException | 明确指定集合名称 |
5. 扩展知识
- MyBatis 底层原理:
#{}通过PreparedStatement实现预编译,${} 直接拼接 SQL - 集合类型选择:
List和Array都支持,但 Map 需特殊处理 - 动态 SQL 进阶:结合
<choose>和<if>实现多条件分支 - 极限优化:对于千万级数据,考虑异步导出或搜索引擎方案
6. 最佳实践总结
- 始终优先使用
#{}避免注入 - 超过 500 个参数时分批查询
- 使用
@Param明确集合变量名 - 查询字段加索引(特别是 JOIN 字段)
- 大数据量场景优先用临时表方案