MyBatis 流式查询详解:ResultHandler 与 Cursor
在业务中,如果一次性查询出百万级数据并返回 List,很容易造成 OOM 或 长时间 GC。 MyBatis 提供了 流式查询(Streaming Query) 能力,让我们可以边读边处理,极大降低内存压力。
1. 什么是流式查询?
普通查询:一次性将全部结果加载到内存,然后再处理。 流式查询:数据库返回一个游标(Cursor),应用端一批一批地从游标读取数据,边读边处理,避免占用大量内存。
适用场景
- 导出大批量数据(CSV、Excel)
- 批量处理(数据同步、数据迁移)
- 实时计算
2. MyBatis 流式查询的两种实现方式
2.1 使用 ResultHandler
ResultHandler 是 MyBatis 提供的经典方式,查询结果不会一次性放到内存,而是每读取一条就调用一次回调方法。
不带参数示例
@Mapper
public interface UserMapper {
@Select("SELECT id, name, age FROM user")
void scanAllUsers(ResultHandler<User> handler);
}
调用:
@Autowired
private UserMapper userMapper;
public void processUsersNoParam() {
userMapper.scanAllUsers(ctx -> {
User user = ctx.getResultObject();
System.out.println(user);
});
}
带参数示例
@Mapper
public interface UserMapper {
@Select("SELECT id, name, age FROM user WHERE age > #{age}")
void scanUsersByAge(@Param("age") int age, ResultHandler<User> handler);
}
调用:
public void processUsersWithParam(int minAge) {
userMapper.scanUsersByAge(minAge, ctx -> {
User user = ctx.getResultObject();
System.out.println(user);
});
}
特点
-
边查边处理,不占用过多内存
-
处理逻辑和查询绑定在一起
-
适合流式消费(文件写入、推送消息)
-
如果收集成 List,内存压力和普通查询差不多
2.2 使用 Cursor(推荐 MyBatis 3.4+)
Cursor 提供了更接近 JDBC ResultSet 的方式,支持 Iterable 迭代。
不带参数示例
@Mapper
public interface UserMapper {
@Select("SELECT id, name, age FROM user")
@Options(fetchSize = Integer.MIN_VALUE) // MySQL 开启流式
Cursor<User> scanAllUsers();
}
调用:
@Transactional
@Transactional
public void getUsersAsList() throws IOException {
try (Cursor<User> cursor = userMapper.scanAllUsers()) {
for (User user : cursor) {
System.out.println(user);
}
}
}
带参数示例
@Mapper
public interface UserMapper {
@Select("SELECT id, name, age FROM user WHERE age > #{age}")
@Options(fetchSize = Integer.MIN_VALUE)
Cursor<User> scanUsersByAge(@Param("age") int age);
}
调用:
@Transactional
@Transactional
public void getUsersByAge(int minAge) throws IOException {
try (Cursor<User> cursor = userMapper.scanUsersByAge(minAge)) {
for (User user : cursor) {
System.out.println(user);
}
}
}
3. Cursor 踩坑:A Cursor is already closed
很多人在用 Cursor 时会遇到:
A Cursor is already closed.
原因
- Cursor 是延迟加载的,必须在 同一个 SqlSession 存活期间 迭代
- 如果你在 mapper 方法中返回 Cursor,却在外部再去遍历,此时 SqlSession 已经被 MyBatis 关闭,Cursor 自然不可用
错误示例
Cursor<User> cursor = userMapper.scanAllUsers(); // 此时 SQLSession 会在方法返回后关闭
for (User user : cursor) { // 这里会报错
...
}
解决办法
- 在同一个方法中迭代,不要把 Cursor 返回到方法外
- 加 @Transactional 保证 SqlSession 在方法执行期间不关闭
- 用 try-with-resources 及时关闭 Cursor
正确示例
@Transactional
public void processCursor() {
try (Cursor<User> cursor = userMapper.scanAllUsers()) {
for (User user : cursor) {
// 处理数据
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
4. 注意事项
-
MySQL 必须设置@Options(fetchSize = Integer.MIN_VALUE) 才能真正流式
-
事务控制:Cursor 必须在事务或 SqlSession 存活期间消费
-
大事务风险:流式处理可能导致事务时间长,要权衡
-
网络延迟:流式每次批量取数,可能比一次性查询多几毫秒,但内存安全
-
收集成 List 慎用:这样会失去流式查询的内存优势
5. 区别
ResultHandler(回调模式):
-
基于观察者模式/回调模式
-
MyBatis 主动推送数据给你的处理器
-
你提供一个处理函数,MyBatis 逐条调用
Cursor(迭代器模式):
-
基于迭代器模式
-
你主动从 Cursor 中拉取数据
-
更符合 Java 集合框架的使用习惯
ResultHandler 更适合:
-
简单的逐条处理场景
-
不需要复杂控制流程的情况
-
希望 MyBatis 完全管理资源的场景
Cursor 更适合:
-
需要复杂处理逻辑的场景
-
需要灵活控制处理流程
-
习惯使用 Java 8 Stream API 的开发者
-
需要与现有迭代处理代码集成
选择 ResultHandler 当:
-
处理逻辑简单直接
-
不需要复杂的流程控制
-
希望代码更紧凑
-
不希望手动管理资源
选择 Cursor 当:
-
需要灵活的流程控制
-
处理逻辑复杂,需要分步骤
-
团队熟悉迭代器模式需要与其他基于迭代器的代码集成
-
希望有更好的异常处理控制
1855

被折叠的 条评论
为什么被折叠?



