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导出示例
-
数据表假设
表名:user
字段:id, username, email, created_at -
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 会在 流式读取时按需取数据,不会一次性加载到内存。
- 实体类
import lombok.Data;
import java.time.LocalDateTime;@Data
public class User {private Long id;private String username;private String email;private LocalDateTime createdAt;
}
- 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);}}
}
- 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;}
}
- 核心要点总结
-
@Transactional(readOnly = true) 必须加,否则 游标会自动关闭。
-
使用 Cursor 遍历,MyBatis 内部会 逐批查询。
-
避免用 List 直接加载大表,否则会 OOM。
-
建议配合 分页游标(比如按 ID 范围分批)进一步优化。
“人的一生会经历很多痛苦,但回头想想,都是传奇”。