当前位置: 首页 > backend >正文

MyBatis流式查询详解

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) { // 这里会报错...
}

解决办法

  1. 在同一个方法中迭代,不要把 Cursor 返回到方法外
  2. 加 @Transactional 保证 SqlSession 在方法执行期间不关闭
  3. 用 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. 注意事项

  1. MySQL 必须设置 @Options(fetchSize = Integer.MIN_VALUE) 才能真正流式
  2. 事务控制:Cursor 必须在事务或 SqlSession 存活期间消费
  3. 大事务风险:流式处理可能导致事务时间长,要权衡
  4. 网络延迟:流式每次批量取数,可能比一次性查询多几毫秒,但内存安全
  5. 收集成 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 查询即可
http://www.xdnf.cn/news/17275.html

相关文章:

  • 系统构成与 Shell 核心:从零认识操作系统的心脏与外壳
  • 机器学习-Logistic Regression
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘transformers’问题
  • AJAX与axios框架
  • 【轮速传感器方向判断原理】
  • Flutter开发 SingleChildScrollView、ScrollController
  • 液体泄漏识别误报率↓76%:陌讯多模态融合算法实战解析
  • camera人脸识别问题之二:【FFD】太阳逆光场景,人像模式后置打开美颜和滤镜,关闭heif拍摄格式对着人脸拍照,成像口红出现位置错误
  • 北京安全员C练习题
  • Xiphos Q8 摄像头板 高性能图像处理板
  • 恒科持续低迷:新能源汽车股下跌成拖累,销量担忧加剧
  • C++编程之旅-- -- --类与对象的奇幻征途之初识篇(一)(了解类的基本用法,计算类大小,分析this指针)
  • 快速上手 Ollama:强大的开源语言模型框架
  • GitLab同步提交的用户设置
  • 论文reading学习记录7 - daily - ViP3D
  • 日本站群服务器与普通日本服务器对比
  • Spring AMQP 入门与实践:整合 RabbitMQ 构建可靠消息系统
  • 【接口自动化测试】---requests模块
  • SpringBoot的profile加载
  • 可编辑51页PPT | 某鞋服品牌集团数字化转型项目建议书
  • 微服务如何保证系统高可用?
  • iOS 签名证书全流程详解,申请、管理与上架实战
  • 腾讯iOA:数据安全的港湾
  • 0_外设学习_ESP8266+云流转(no 0基础)
  • 最新的GPT5效果如何,我试了一下(附加GPT5大模型免费使用方法)
  • 力扣-189.轮转数组
  • 秋招笔记-8.8
  • 《Leetcode》-面试题-hot100-链表
  • django uwsgi启动报错failed to get the Python codec of the filesystem encoding
  • Android 系统的安全 和 三星安全的区别