Mybatis流式查询

流式查询 指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值