流式查询 指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。
如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。
流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。
Cursor和ResultHandler功能是一样的,Cursor比ResultHandler多包装了一层,实际Cursor调用时候,还是和ResultHandler调用的逻辑一样。
在使用的时候,Cursor需要自己关闭连接,具体怎么执行,可查看其他博客
ResultHandler使用时要注意,fetchSize字段的值根据需要设置,如果一次查询的数据量没超过堆栈上限,可以设置100-1000之间的值(具体看业务)提高查询效率,这个值的意思是,每次查询从数据库一次取多少条数据,然后存放在内存里,然后在通过从内存拿这个数据。
如果要实现查询百万以上数据,还不想被爆内存可以把fetchSize设置为Integer.MIN_VALUE,具体为什么,我也没搞清楚,因为涉及要去数据库底层看逻辑,能力有限,暂时没搞明白。
链接:https://juejin.cn/post/7184442693605392441
Mybatis实现流式查询
接下来就是实现流失查询的具体过程。
在mapper映射文件中,编写流式查询的逻辑。
<select id="selectFetchSize" fetchSize="-2147483648" resultSetType="FORWARD_ONLY" resultType="com.qf.shop.cms.entity.TContent">
select * from t_content
</select>
在mapper接口文件中添加selectFetchSize方法。
// 参数 ResultHandler 是一个回调接口,也就是从游标中获得一条数据就会回调接口中的方法
void selectFetchSize(ResultHandler<TContent> handler);
自己编写一个类实现ResultHandler接口,在该接口中定义从游标获得一条数据后的回调逻辑。
/**
* 通过流式查询每获得一条数据的回调类
*/
public class TContentResultHandler implements ResultHandler<TContent> {
/**
* 这里每集满1000条数据 往硬盘的excel文件中追加一次数据
*/
private final static int BATCH_SIZE = 1000;
/**
* 计数器
*/
private int size=0;
/**
* 存储每批数据的临时容器
*/
private List<TContent> tContents = new ArrayList<>();
/**
* 每从流式查询中获得一行结果,就会调用一次这个方法
* @param resultContext
*/
@Override
public void handleResult(ResultContext<? extends TContent> resultContext){
// 这里获取流式查询每次返回的单条结果
TContent resultObject = resultContext.getResultObject();
// 你可以看自己的项目需要分批进行处理或者单个处理,这里以分批处理为例
tContents.add(resultObject);
size++;
if (size == BATCH_SIZE) {
// 如果集满1000条就往文件中写一次
handle();
}
}
/**
* 集满1000条 执行一次的逻辑
*/
private void handle() {
try {
// 在这里可以对你获取到的批量结果数据进行需要的业务处理
// 这里的业务是 往文件中写一次
} finally {
// 处理完每批数据后后将临时清空
size = 0;
tContents.clear();
}
}
/**
* 这个方法给外面调用,用来完成最后一批数据处理
*/
public void end(){
handle();// 处理最后一批不到BATCH_SIZE的数据
}
}
在业务逻辑(service)层调用流式查询方法。
@Autowired
private TContentMapper contentMapper;
public void streamQuery(){
// 生成流式查询的回调对象
TContentResultHandler tContentResultHandler = new TContentResultHandler();
// 调用流式查询
contentMapper.selectFetchSize(tContentResultHandler);
// 执行完最后一批数据的逻辑
tContentResultHandler.end();
}
方式一: 基于mybatis的游标规则 类似于oracle的fetchsize缓冲区
package com.abi.mapper;
import com.abi.entity.WholesalerBase;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.ResultType;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.session.ResultHandler;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface WholesalerBaseMapper extends BaseMapper<WholesalerBase> {
/**
* 流式查询
* @param resultHandler
*/
@ResultType(WholesalerBase.class)
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
void getBaseList(ResultHandler<WholesalerBase> resultHandler);
/**
* 流式查询 查询上一周的更新数据
* @param resultHandler
*/
@ResultType(WholesalerBase.class)
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
void getBaseWeekList(ResultHandler<WholesalerBase> resultHandler);
/**
* 获取指定状态奖励的结算记录
* 流式查询
*/
@ResultType(TaskSettleLogDTO.class)
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
void getNotIssuedCycleSettleLogs(
@Param("rewardStatuses") List<Integer> rewardStatuses,
@Param("cycleType") Integer cycleType,
@Param("starTime") LocalDateTime starTime,
@Param("endTime") LocalDateTime endTime,
ResultHandler<TaskSettleLogDTO> resultHandler
);
}
基于Cursor构建的方式
方式二: SqlSession 方式
mapper层
@Select("select * from wholesaler_base base WHERE base.verify_status != 3 " +
" and base.deleted=0 and base.level=2")
Cursor<WholesalerBase> scan();
测试程序
@Test
public void t5() {
SqlSession sqlSession = null;
try {
sqlSession = this.sqlSessionFactory.openSession();
Cursor<WholesalerBase> cursors =
sqlSession.getMapper(WholesalerBaseMapper.class).scan();
cursor.forEach(base -> {
//业务需求
System.out.println("base = " + base);
});
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
方式三: TransactionTemplate
测试程序
@Resource
private PlatformTransactionManager platformTransactionManager;
@Test
void t3() {
TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);
transactionTemplate.execute(status -> {
try (Cursor<WholesalerBase> cursor = wholesalerBaseMapper.scan()) {
cursor.forEach(base -> {
//需求代码
System.out.println("base = " + base);
});
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
}
方式四: @Transactional 注解
@Test
@Transactional(readOnly = true)
void t4() {
try (Cursor<WholesalerBase> cursor = wholesalerBaseMapper.scan()) {
cursor.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
流式查询中满1000条数据清空
@Transactional(readOnly = true)
public void test(Set<String> codes) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("poc");
int num = 1000;
Set<String> pocMiddleIdList = new HashSet<>();
Map<String, String> pocMiddleIdOrgMap = new HashMap<>();
this.pocBaseInfoMapper.getPocBaseInfoList(codes, (resultContext) -> {
PocBaseInfo resultObject = resultContext.getResultObject();
System.out.println(JSONUtil.toJsonStr(resultObject));
pocMiddleIdList.add(resultObject.getPocMiddleId());
pocMiddleIdOrgMap.put(resultObject.getPocMiddleId(), resultObject.getOrgCode());
if (pocMiddleIdList.size() == num) {
//todo
pocMiddleIdList.clear();
}
});
stopWatch.stop();
//测试进行时间
System.out.println(stopWatch.prettyPrint());
}
读
分页查询我们举个实际例子。下面是一个 Mapper 类:
@Mapperpublic interface FooMapper {
@Select("select * from foo limit #{limit}")
Cursor<Foo> scan(@Param("limit") int limit);
}
方法 scan() 是一个非常简单的查询。通过指定 Mapper 方法的返回值为 Cursor 类型,MyBatis 就知道这个查询方法一个流式查询。
然后我们再写一个 SpringMVC Controller 方法来调用 Mapper(无关的代码已经省略):
@GetMapping("foo/scan/0/{limit}")
public void scanFoo0(@PathVariable("limit") int limit) throws Exception {
try (Cursor<Foo> cursor = fooMapper.scan(limit)) { // 1
cursor.forEach(foo -> {}); // 2
}}
假设fooMapper 是 @Autowired 进来的。注释 1 处调用 scan 方法,得到 Cursor 对象并保证它能最后关闭;2 处则是从 cursor 中取数据。
上面的代码看上去没什么问题,但是执行 scanFoo0() 时会报错:
java.lang.IllegalStateException: A Cursor is already closed.
这是因为我们前面说了在取数据的过程中需要保持数据库连接,而 Mapper 方法通常在执行完后连接就关闭了,因此 Cusor 也一并关闭了。
所以,解决这个问题的思路不复杂,保持数据库连接打开即可。我们至少有三种方案可选。
方案一:SqlSessionFactory
我们可以用 SqlSessionFactory 来手工打开数据库连接,将 Controller 方法修改如下:
@GetMapping("foo/scan/1/{limit}")public void scanFoo1(@PathVariable("limit") int limit) throws Exception { try ( SqlSession sqlSession = sqlSessionFactory.openSession(); // 1 Cursor<Foo> cursor = sqlSession.getMapper(FooMapper.class).scan(limit) // 2 ) { cursor.forEach(foo -> { }); }}
上面的代码中,1 处我们开启了一个 SqlSession (实际上也代表了一个数据库连接),并保证它最后能关闭;2 处我们使用 SqlSession 来获得 Mapper 对象。这样才能保证得到的 Cursor 对象是打开状态的。
方案二:TransactionTemplate
在 Spring 中,我们可以用 TransactionTemplate 来执行一个数据库事务,这个过程中数据库连接同样是打开的。代码如下:
@GetMapping("foo/scan/2/{limit}")public void scanFoo2(@PathVariable("limit") int limit) throws Exception { TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); // 1 transactionTemplate.execute(status -> { // 2 try (Cursor<Foo> cursor = fooMapper.scan(limit)) { cursor.forEach(foo -> { }); } catch (IOException e) { e.printStackTrace(); } return null; });}
上面的代码中,1 处我们创建了一个 TransactionTemplate 对象(此处 transactionManager 是怎么来的不用多解释,本文假设读者对 Spring 数据库事务的使用比较熟悉了),2 处执行数据库事务,而数据库事务的内容则是调用 Mapper 对象的流式查询。注意这里的 Mapper 对象无需通过 SqlSession 创建。
方案三:@Transactional 注解
这个本质上和方案二一样,代码如下:
@GetMapping("foo/scan/3/{limit}")
@Transactionalpublic
void scanFoo3(@PathVariable("limit") int limit) throws Exception {
try (Cursor<Foo> cursor = fooMapper.scan(limit)) {
cursor.forEach(foo -> { }); }
}
它仅仅是在原来方法上面加了个 @Transactional 注解。这个方案看上去最简洁,但请注意 Spring 框架当中注解使用的坑:只在外部调用时生效 。在当前类中调用这个方法,依旧会报错。
以上是三种实现 MyBatis 流式查询的方法
————————————————
版权声明:本文为CSDN博主「污鸦嘴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_61811711/article/details/125604711