本文主要是介绍MyBatis流式查询两种实现方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《MyBatis流式查询两种实现方式》本文详解MyBatis流式查询,通过ResultHandler和Cursor实现边读边处理,避免内存溢出,ResultHandler逐条回调,Cursor支持迭代...
MyBatis 流式查询详解:ResultHandler 与 Cursor
在业务中,如果一次性查询出百万级数据并返回 List
,很容易造成 OOM 或 长时间 GC。
MyBatis 提供了 流式查询(Streaming Query) 能力,让我们可以边读边处理,极大降低内存压力。
1. 什么是流式查询?
普通查询:一次性将全部结果加载到内存,然后再处理。
流式查询:数据库返回一个游标(Cursor),应用端一批一批地从游标读取数据,边读边处理,避免占用大量内存。
适用场景
- 导出大批量数据(CSV、Excel)
- 批量处理(数据同步、数据迁移)
- 实时计算
2. MyBatis 流式查询的两种实现方式
2.1 使用 ResultHandler
ResultHandler 是 MyBatis 提供的经典方式,查询结果不会一次性放到android内存,而是每读取一条就调用一次回调方法。
不带参数示例
@Mapper
public interface UserMapper {
@Select("SELECT id, name, age FROM user")
void scanAlhttp://www.chinasem.cnlUsers(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") inhttp://www.chinasem.cnt age); }
调用:
@Transactional @Transactional public void getUsersByAge(int minAge) throws IOException { try (Cursor<User> cursor = userMapper.scanUsersByAge(minAge)) { for (User http://www.chinasem.cnuser : 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 当:
- 需要灵活的流程控制
- 处理逻辑复杂,需要分步骤
- 团队熟悉迭代器模式
- 需要与其他基于迭代器的代码集成
- 希望有更好的异常处理控制
6. 总结
- ResultHandler:更灵活,回调式消费,适合不需要一次性得到全部结果
- Cursor:可迭代,语法直观,但必须在 SqlSession 存活期间消费,否则就会遇到
A Cursor is already closed
- 带参数查询:ResultHandler 和 Cursor 都支持,只需在 mapper 方法加参数
- 实战建议:
- 大批量导出、批量同步 → Cursor
- 条件过滤、部分收集 → ResultHandler
- 不需要流式直接用普通 List 查询即可
到此这篇关于MyBatis流式查询两种实现方式的文章就介绍到这了,更多相关mybatis流式查询内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!
这篇关于MyBatis流式查询两种实现方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!