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 当:
- 需要灵活的流程控制
- 处理逻辑复杂,需要分步骤
- 团队熟悉迭代器模式
- 需要与其他基于迭代器的代码集成
- 希望有更好的异常处理控制
6. 总结
- resulthandler:更灵活,回调式消费,适合不需要一次性得到全部结果
- cursor:可迭代,语法直观,但必须在 sqlsession 存活期间消费,否则就会遇到
a cursor is already closed
- 带参数查询:resulthandler 和 cursor 都支持,只需在 mapper 方法加参数
- 实战建议:
- 大批量导出、批量同步 → cursor
- 条件过滤、部分收集 → resulthandler
- 不需要流式直接用普通 list 查询即可
到此这篇关于mybatis流式查询两种实现方式的文章就介绍到这了,更多相关mybatis流式查询内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论