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

MyBatis 流式查询详解

文章目录

    • 1. 为什么需要流式查询?
    • 2. MyBatis 流式查询的实现方式
      • 2.1 ResultHandler 模式
      • 2.2 Cursor 模式(推荐)
    • 3. 配置优化
      • 3.1. fetchSize
      • 3.2.resultSetType
      • 3.2.事务管理
    • 4. 适用场景
    • 5. 总结和注意事项
      • 5.1 总结
      • 5.2 注意事项
    • 6. 附录完整的csv导出示例

本文详细讲解 MyBatis 的流式查询(Streaming Query),包括原理、使用方式、适用场景和注意事项。

1. 为什么需要流式查询?

在 MyBatis 中,普通的查询(selectList)会一次性将数据库返回的所有数据加载到内存中(JVM 内存)。

👉 如果数据量很大(几万、几十万甚至上百万行),就可能导致:

  • 内存溢出(OOM)

  • 响应速度慢

  • GC 压力过大

而流式查询的目标是:像数据库游标一样,逐行(或逐批)读取数据并处理,而不是一次性加载全部。

2. MyBatis 流式查询的实现方式

MyBatis 提供了 ResultHandler 与 Cursor 两种流式查询方式:

2.1 ResultHandler 模式

  • 原理:查询结果逐条返回,并调用 handleResult 方法处理。

  • 适合:边查边处理,不需要保留全部数据。

示例:

sqlSession.select("com.example.UserMapper.selectAll", null, new ResultHandler<User>() {@Overridepublic void handleResult(ResultContext<? extends User> resultContext) {User user = resultContext.getResultObject();System.out.println("处理用户: " + user.getName());// 在这里写入文件 / 发送消息队列 / 流式处理}
});

这种方式不会把所有结果放在内存中,而是逐条回调。

2.2 Cursor 模式(推荐)

  • MyBatis 3.5+ 提供了 Cursor,类似 JDBC ResultSet 游标。

  • 支持 try-with-resources,用完后自动关闭。

  • 可以用 流式 API(iterator/forEach) 逐行遍历。

示例:


try (Cursor<User> cursor = sqlSession.selectCursor("com.example.UserMapper.selectAll")) {for (User user : cursor) {System.out.println("处理用户: " + user.getName());}
}

如果你使用 MyBatis-Mapper 接口,也可以这样写:

@Select("SELECT * FROM user")
@Options(fetchSize = 1000, resultSetType = ResultSetType.FORWARD_ONLY)
Cursor<User> selectAll();

3. 配置优化

为了让流式查询真正生效,还需要在 JDBC 层进行配置:

3.1. fetchSize

建议设置成 1000 或 5000,避免一次性取太多。

例如:

@Options(fetchSize = 1000, resultSetType = ResultSetType.FORWARD_ONLY)

注意:MySQL 的 fetchSize 特殊,需要 stmt.setFetchSize(Integer.MIN_VALUE) 才能启用流式结果。

3.2.resultSetType

推荐使用:

ResultSetType.FORWARD_ONLY

避免随机访问,提高效率。

3.2.事务管理

必须开启事务(即使是只读)。

例如:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE, false);

4. 适用场景

  • 大数据导出(避免一次性查到内存)

  • ETL 数据处理

  • 日志 / 审计记录遍历

  • 批量推送消息

5. 总结和注意事项

5.1 总结

  • 小数据 → 用 selectList 就行。

  • 大数据(>10w 行) → 推荐用 流式查询(Cursor/ResultHandler)。

  • MySQL 要特别注意 fetchSize=Integer.MIN_VALUE。

5.2 注意事项

  • 流式查询需要事务保持打开状态
    也就是说:如果你 sqlSession.close() 或提交事务,游标会失效。

  • 游标必须手动关闭
    否则可能导致连接泄露(推荐 try-with-resources)。

  • 与分页不同

    分页是数据库端每次查一部分。

    流式查询是一次查询,逐行提取。

  • MySQL 的特殊情况

必须设置 fetchSize=Integer.MIN_VALUE,才会启用服务器端游标。

示例:

@Select("SELECT * FROM user")
@Options(fetchSize = Integer.MIN_VALUE, resultSetType = ResultSetType.FORWARD_ONLY)
Cursor<User> streamAllUsers();

6. 附录完整的csv导出示例

  1. 数据表假设

    表名:user
    字段:id, username, email, created_at

  2. Mapper 接口(流式查询)

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.cursor.Cursor;@Mapper
public interface UserMapper {@Select("SELECT id, username, email, created_at FROM user ORDER BY id")Cursor<User> streamAllUsers();}

⚠️ 注意:

  • 返回类型是 org.apache.ibatis.cursor.Cursor

  • MyBatis 会在 流式读取时按需取数据,不会一次性加载到内存。

  1. 实体类
import lombok.Data;
import java.time.LocalDateTime;@Data
public class User {private Long id;private String username;private String email;private LocalDateTime createdAt;
}
  1. Service 实现(导出到 CSV)
import com.opencsv.CSVWriter;
import org.apache.ibatis.cursor.Cursor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.io.FileWriter;
import java.io.IOException;@Service
public class UserExportService {private final UserMapper userMapper;public UserExportService(UserMapper userMapper) {this.userMapper = userMapper;}/*** 导出用户到 CSV(流式处理,避免OOM)*/@Transactional(readOnly = true)public void exportUsersToCsv(String filePath) {try (Cursor<User> cursor = userMapper.streamAllUsers();CSVWriter writer = new CSVWriter(new FileWriter(filePath))) {// 写表头writer.writeNext(new String[]{"ID", "Username", "Email", "CreatedAt"});// 逐行写入for (User user : cursor) {writer.writeNext(new String[]{String.valueOf(user.getId()),user.getUsername(),user.getEmail(),String.valueOf(user.getCreatedAt())});}writer.flush();System.out.println("✅ 导出完成: " + filePath);} catch (IOException e) {throw new RuntimeException("导出 CSV 失败", e);}}
}
  1. Controller 示例(触发导出)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class UserExportController {private final UserExportService exportService;public UserExportController(UserExportService exportService) {this.exportService = exportService;}@GetMapping("/export-users")public String exportUsers() {String filePath = "/tmp/users.csv";exportService.exportUsersToCsv(filePath);return "导出成功: " + filePath;}
}
  1. 核心要点总结
  • @Transactional(readOnly = true) 必须加,否则 游标会自动关闭。

  • 使用 Cursor 遍历,MyBatis 内部会 逐批查询。

  • 避免用 List 直接加载大表,否则会 OOM。

  • 建议配合 分页游标(比如按 ID 范围分批)进一步优化。

在这里插入图片描述


“人的一生会经历很多痛苦,但回头想想,都是传奇”。


http://www.dtcms.com/a/352026.html

相关文章:

  • 使用 mcp-use 构建极简 Web 自动化测试智能体「喂饭教程」
  • 前端漏洞(上)- CORS漏洞
  • 静态HTML网页模板设计与实现
  • python基础-面向对象编程(OOP)
  • 我们来学mysql -- safe启动
  • Mysql——日志
  • 【45页PPT】制造行业数据资产运营平台需求方案(附下载方式)
  • 【科研绘图系列】R语言在海洋生态学中的应用:浮游植物糖类组成与溶解性有机碳的关系
  • OpenCV打开视频函数VideoCapture使用详解
  • Linux桌面主题的安装
  • 33.ansible 比较重要的配置文件
  • 运算符(2)
  • 审核问题——鸿蒙审核返回安装失败,可以尝试云调试
  • timedatectl查看时间同步
  • Windows本地部署大模型方式对比
  • 约束满足问题(CSP)--搜索算法在实际场景中的应用
  • 深度学习篇---LeNet-5
  • 国产银河麒麟SP1桌面系统如何免密登录系统
  • Rust:函数与控制流
  • MATLAB在生态环境数据处理与分析中的应用
  • 基于MATLAB的雷达系统设计中的信号处理程序
  • Java:Docx4j类库简介及使用
  • 在 Vue 中嵌入 Unity WebGL 并实现双向通信
  • 有 100W 个数,有一个函数是可以高效查找并删除某个数,问应该用什么数据结构去存这 100W 个数
  • 文献阅读笔记【雷达信号分选】:基于机器学习的雷达信号分选方法综述
  • 在python 代码中调用rust 源码库操作步骤
  • Excel跨sheet检索提取信息
  • 最简洁yolov8 C++配置教程
  • Leetcode+Java+dpI
  • 汇智焕彩,聚势创新 - openKylin 2.0 SP2正式发布!