MyBatis--流式查询

496 阅读5分钟

一、什么是流式查询

使用mybatis作为持久层的框架时,通过mybatis执行查询数据的请求执行成功后,mybatis返回的结果集不是一个集合或对象,而是一个迭代器,可以通过遍历迭代器来取出结果集,避免一次性取出大量的数据而占用太多的内存。

二、流式查询的好处

如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。

流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。

三、MyBatis流式查询接口介绍

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:

  • Cursor 是可关闭的;
  • Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  • isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  • isConsumed():用于判断查询结果是否全部取完。
  • getCurrentIndex():返回已经获取了多少条数据
public interface Cursor<T> extends Closeable,Iterable<T> {
    //判断cursor是否正处于打开状态
    //当返回为true,表示cursor已经开始从数据库里刷新数据了
    boolean isOpen();
    
    //判断查询结果是否全部被读取完
    //当返回为ture,则表示SQL匹配的全部数据被消费完了
    boolean isConsumed();
    
    //查询已读取数据在全部数据里的索引位置
    //第一条数据的索引位置为0,当返回索引位置为-1时,表示已经没有数据可以读取
}

四、代码介绍

mybatis的所谓流式查询,就是服务端程序查询数据的过程中,与远程数据库一直保持连接,不断的去数据库拉取数据,提交事务并关闭sqlsession后,数据库连接断开,停止数据拉取,需要注意的是使用这种方式,需要自己手动维护sqlsession和事务的提交。

1、实现方式很简单,原来返回的类型是集合或对象,流式查询返回的的类型Cursor,泛型内表示实际的类型,其他没有变化;

@Mapper
public interface PersonDao{
    Cursor<Person> selectByCursor();
    
    Interger queryCount();
}
<select id="selectByCursor" resultMap="personMap">
    select * from person order by id desc
</select>

<select id="queryCount" resultType="java.lang.Integer">
    select count(*) from person
</select>

2、dao层向service层返回的是Cursor类型对象,只要不提交关闭sqlsession,服务端程序就可以一直从数据数据库读取数据,直到查询sql匹配到数据全部读取完; 示例里的主要业务逻辑是:从person表中读取所有的人员信息数据,然后按照每1000条数据为一组,读取到内存里进行处理,以此类推,直到查询sql匹配到数据全部处理完,再提交事务,关闭sqlSession;

@Service
@Slf4j
public class PersonServiceImpl implements IPersonService {
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void getOneByAsync() throws InterruptedException {
    
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //使用sqlSessionFactory打开一个sqlSession,在没有读取完数据之前不要提交事务或关闭sqlSession
                log.info("----开启sqlSession");
                SqlSession sqlSession = sqlSessionFactory.openSession();
                 try {
                 
                     //获取到指定mapper
                     PersonDao mapper = sqlSession.getMapper(PersonDao.class);
                     
                     //调用指定mapper的方法,返回一个cursor
                     Cursor<Person> cursor = mapper.selectByCursor();
                     
                     if (cursor == null){
                         return;                     
                     }
                     
                     //查询数据总量
                     Integer total = mapper.queryCount();
                     
                     //定义一个list,用来从cursor中读取数据,每读取够1000条的时候,开始处理这批数据;
                     //当前批数据处理完之后,清空list,准备接收下一批次数据;直到大量的数据全部处理完;
                     List<Person> personList = new ArrayList<>();
                     
                     int i = 0;
                                        
                         for (Person person : cursor) {
                             //
                             if (personList.size() < 1000) {

                                 personList.add(person);
                             } else if (personList.size() == 1000) {
                                 ++i;
                               
                                 Thread.sleep(1000);//休眠1s模拟处理数据需要消耗的时间;
                                
                                 personList.clear();
                                 personList.add(person);
                             }
                             
                             if (total == (cursor.getCurrentIndex() + 1)) {
                                 ++i;
                                
                                 Thread.sleep(1000);//休眠1s模拟处理数据需要消耗的时间;
                            
                                 personList.clear();
                             }
                             //
                         }
                         if (cursor.isConsumed()) {
                             log.info("----查询sql匹配中的数据已经消费完毕!");
                         }
                     
                     sqlSession.commit();
                     log.info("----提交事务");
                 }catch (Exception e){
                     e.printStackTrace();
                     sqlSession.rollback();
                 }
                 finally {
                     if (sqlSession != null) {
                         //全部数据读取并且做好其他业务操作之后,提交事务并关闭连接;
                         sqlSession.close();
                         log.info("----关闭sqlSession");  
                     }
                 }
                
            }
        }).start();
    }
}

mybatis的流式查询的本意,是避免大量数据的查询而导致内存溢出,因此dao层查询返回的是一个迭代器(Cursor),可以每次从迭代器中取出一条查询结果,在实际业务开发过程中,即是根据实际的jvm内存大小,从迭代器中取出一定数量的数据后,再进行数据处理,待处理完之后,继续取出一定数据再处理,以此类推直到全部数据处理完,这样做的最大好处就是能够降低内存使用和垃圾回收器的负担,使数据处理的过程相对更加高效、可控,内存溢出的风险较小;好处很明显,缺点也很就明显,处理的时间可能会变长,需要引入多线程异步操作,并且在迭代器遍历和数据处理的过程中,数据库连接不能断开,即当前sqlSession要保持持续打开状态,一量断开,数据读取就会中断,所以关于这块的处理,使用mybatis原生的sqlSession进行手动查询、提交事务、回滚和关闭sqlSession最为稳妥、最简单;