这个问题结合你前面的 MySQL 索引话题,大概率是在说 MyBatis Mapper 查询结果的处理方式。我按使用场景从常见到高级给你梳理:


一、最常见:Mapper 返回 List<T> + 业务层循环

1️⃣ foreach(增强 for 循环)

  1. List<InfraApiAccessLog> list = infraApiAccessLogMapper.selectByCondition(param);
  2. for (InfraApiAccessLog log : list) {
  3. // 逐条处理
  4. System.out.println(log.getOperateName());
  5. }

2️⃣ 传统 for 循环

  1. List<InfraApiAccessLog> list = mapper.selectByCondition(param);
  2. for (int i = 0; i < list.size(); i++) {
  3. InfraApiAccessLog log = list.get(i);
  4. // 处理,需要下标时用这个
  5. }

3️⃣ Iterator 迭代器

  1. Iterator<InfraApiAccessLog> it = list.iterator();
  2. while (it.hasNext()) {
  3. InfraApiAccessLog log = it.next();
  4. // 处理
  5. }

4️⃣ Java 8 Stream API

  1. list.forEach(log -> {
  2. // 处理
  3. });
  4. // 或
  5. list.stream()
  6. .filter(log -> "下载文件".equals(log.getOperateName()))
  7. .forEach(log -> { /* 处理 */ });

二、大数据量:游标(Cursor)流式读取

当结果集几十万/上百万行时,List 方式会 OOM,用 Cursor:

5️⃣ Cursor<T> + foreach(MyBatis 3.4+)

  1. // Mapper 接口
  2. @Select("SELECT * FROM infra_api_access_log WHERE create_time > #{start}")
  3. Cursor<InfraApiAccessLog> selectByCursor(@Param("start") String start);
  1. // 调用处
  2. try (Cursor<InfraApiAccessLog> cursor = mapper.selectByCursor("2026-06-20")) {
  3. for (InfraApiAccessLog log : cursor) {
  4. // 逐行读取,内存中只保留当前行
  5. System.out.println(log);
  6. }
  7. }
  8. // 自动关闭

⚠️ 注意:Cursor 必须在 同一个 SqlSession 生命周期内 遍历,不能在方法返回后遍历(除非用 SqlSessionTemplate 的流式查询)。


三、最底层:ResultHandler 回调

6️⃣ ResultHandler 逐行处理(最省内存)

  1. // Mapper 接口
  2. @Select("SELECT * FROM infra_api_access_log WHERE create_time > #{start}")
  3. void selectByResultHandler(@Param("start") String start, ResultHandler<InfraApiAccessLog> handler);
  1. // 调用处
  2. mapper.selectByResultHandler("2026-06-20", context -> {
  3. InfraApiAccessLog log = (InfraApiAccessLog) context.getResultObject();
  4. // 逐行处理,不会一次性加载到内存
  5. System.out.println(log.getOperateName());
  6. });

底层原理:MyBatis 每从 JDBC ResultSet 读出一行就回调一次,完全不缓存结果集。


四、分页循环(经典模式)

7️⃣ PageHelper / MyBatis-Plus 分页 + 循环

  1. int pageNum = 1;
  2. int pageSize = 1000;
  3. while (true) {
  4. PageHelper.startPage(pageNum, pageSize);
  5. List<InfraApiAccessLog> list = mapper.selectByCondition(param);
  6. if (list.isEmpty()) break;
  7. for (InfraApiAccessLog log : list) {
  8. // 处理
  9. }
  10. pageNum++;
  11. }

或 MyBatis-Plus 的 Page 对象:

  1. Page<InfraApiAccessLog> page = new Page<>(1, 1000);
  2. while (true) {
  3. page = mapper.selectPage(page, wrapper);
  4. if (page.getRecords().isEmpty()) break;
  5. // 处理 records
  6. page.setCurrent(page.getCurrent() + 1);
  7. }

五、XML 里的 <foreach>(容易混淆)

8️⃣ MyBatis <foreach> 标签

这个是拼接 SQL 用的,不是读取结果:

  1. <select id="selectByIds" resultType="InfraApiAccessLog">
  2. SELECT * FROM infra_api_access_log
  3. WHERE id IN
  4. <foreach collection="ids" item="id" open="(" close=")" separator=",">
  5. #{id}
  6. </foreach>
  7. </select>

它生成的是 WHERE id IN (1,2,3,...) 这种 SQL,不是遍历结果集。


六、对比总结

方式 内存占用 适用场景 复杂度
List<T> + foreach 高(全量加载) 小结果集(几百~几万) ⭐ 最简单
Stream 处理 List 高(全量加载) 需要过滤/转换的小结果集 ⭐⭐
Cursor<T> + foreach 低(逐行) 大结果集,需遍历 ⭐⭐⭐
ResultHandler 最低(逐行回调) 超大结果集,流式处理 ⭐⭐⭐⭐
分页循环 低(每页加载) 大结果集 + 需要更新/删除 ⭐⭐⭐

💡 选择建议

  1. 结果集 < 1万行 → 直接 List + foreach ✅
  2. 结果集 1万~50万行 → 分页循环 或 Cursor ✅
  3. 结果集 > 50万行 → Cursor 或 ResultHandler ✅
  4. 需要边读边更新 → 分页循环(避免游标期间锁表太久)✅