Mybatis之批處理流式查詢(xún)

1 批處理查詢(xún)

1.1 引言

動(dòng)態(tài)數(shù)據(jù)導(dǎo)出是一般項(xiàng)目都會(huì)涉及到的功能癌瘾。它的基本實(shí)現(xiàn)邏輯就是從mysql查詢(xún)數(shù)據(jù)觅丰,加載到內(nèi)存,然后從內(nèi)存創(chuàng)建 excel 或者 csv 柳弄,以流的形式響應(yīng)給前端舶胀。但是全量加載不可行概说,那我們的目標(biāo)就是如何實(shí)現(xiàn)數(shù)據(jù)的分批加載了碧注。實(shí)事上,Mysql本身支持 Stream 查詢(xún)糖赔,我們可以通過(guò)Stream流獲取數(shù)據(jù)萍丐,然后將數(shù)據(jù)逐條刷入到文件中,每次刷入文件后再?gòu)膬?nèi)存中移除這條數(shù)據(jù)放典,從而避免OOM逝变。
由于采用了數(shù)據(jù)逐條刷入文件,而且數(shù)據(jù)量達(dá)到百萬(wàn)級(jí)奋构,所以文件格式就不要采用 excel 了壳影,excel2007最大才支持104萬(wàn)行的數(shù)據(jù)。這里推薦弥臼,以csv代替excel

1.2 流式查詢(xún)

1.2.1 定義

流式查詢(xún) 指的是查詢(xún)成功后不是返回一個(gè)集合而是返回一個(gè)迭代器宴咧,應(yīng)用每次從迭代器取一條查詢(xún)結(jié)果。流式查詢(xún)的好處是能夠降低內(nèi)存使用径缅。
如果沒(méi)有流式查詢(xún)掺栅,我們想要從數(shù)據(jù)庫(kù)取 1000 萬(wàn)條記錄而又沒(méi)有足夠的內(nèi)存時(shí),就不得不分頁(yè)查詢(xún)纳猪,而分頁(yè)查詢(xún)效率取決于表設(shè)計(jì)氧卧,如果設(shè)計(jì)的不好,就無(wú)法執(zhí)行高效的分頁(yè)查詢(xún)氏堤。因此流式查詢(xún)是一個(gè)數(shù)據(jù)庫(kù)訪問(wèn)框架必須具備的功能沙绝。

MyBatis 中使用流式查詢(xún)避免數(shù)據(jù)量過(guò)大導(dǎo)致 OOM ,但在流式查詢(xún)的過(guò)程當(dāng)中,數(shù)據(jù)庫(kù)連接是保持打開(kāi)狀態(tài)的宿饱,因此要注意的是:

  • 執(zhí)行一個(gè)流式查詢(xún)后熏瞄,數(shù)據(jù)庫(kù)訪問(wèn)框架就不負(fù)責(zé)關(guān)閉數(shù)據(jù)庫(kù)連接了,需要應(yīng)用在取完數(shù)據(jù)后自己關(guān)閉谬以。
  • 必須先讀惹恳(或關(guān)閉)結(jié)果集中的所有行,然后才能對(duì)連接發(fā)出任何其他查詢(xún)为黎,否則將引發(fā)異常邮丰。

為什么要用流式查詢(xún)?

如果有一個(gè)很大的查詢(xún)結(jié)果需要遍歷處理铭乾,又不想一次性將結(jié)果集裝入客戶端內(nèi)存剪廉,就可以考慮使用流式查詢(xún);
分庫(kù)分表場(chǎng)景下炕檩,單個(gè)表的查詢(xún)結(jié)果集雖然不大斗蒋,但如果某個(gè)查詢(xún)跨了多個(gè)庫(kù)多個(gè)表,又要做結(jié)果集的合并笛质、排序等動(dòng)作泉沾,依然有可能撐爆內(nèi)存;詳細(xì)研究了sharding-sphere的代碼不難發(fā)現(xiàn)妇押,除了group by與order by字段不一樣之外跷究,其他的場(chǎng)景都非常適合使用流式查詢(xún),可以最大限度的降低對(duì)客戶端內(nèi)存的消耗敲霍。

1.2.2 流式查詢(xún)接口

MyBatis 提供了一個(gè)叫 org.apache.ibatis.cursor.Cursor 的接口類(lèi)用于流式查詢(xún)俊马,這個(gè)接口繼承了 java.io.Closeablejava.lang.Iterable 接口,由此可知:
Cursor 是可關(guān)閉的肩杈;Cursor 是可遍歷的柴我。

除此之外,Cursor 還提供了三個(gè)方法:

  • isOpen(): 用于在取數(shù)據(jù)之前判斷 Cursor 對(duì)象是否是打開(kāi)狀態(tài)扩然。只有當(dāng)打開(kāi)時(shí) Cursor 才能取數(shù)據(jù)艘儒;
  • isConsumed(): 用于判斷查詢(xún)結(jié)果是否全部取完。
  • getCurrentIndex(): 返回已經(jīng)獲取了多少條數(shù)據(jù)

使用流式查詢(xún)与学,則要保持對(duì)產(chǎn)生結(jié)果集的語(yǔ)句所引用的表的并發(fā)訪問(wèn)彤悔,因?yàn)槠洳樵?xún)會(huì)獨(dú)占連接,所以必須盡快處理

1.2.3 使用流式查詢(xún)關(guān)閉問(wèn)題

我們舉個(gè)實(shí)際例子索守。下面是一個(gè) Mapper 類(lèi):

@Mapper
public interface FooMapper {
    @Select("select * from foo limit #{limit}")
    Cursor<Foo> scan(@Param("limit") int limit);
}

方法 scan() 是一個(gè)非常簡(jiǎn)單的查詢(xún)晕窑。通過(guò)指定 Mapper 方法的返回值為 Cursor 類(lèi)型,MyBatis 就知道這個(gè)查詢(xún)方法一個(gè)流式查詢(xún)卵佛。
然后我們?cè)賹?xiě)一個(gè) SpringMVC Controller 方法來(lái)調(diào)用 Mapper(無(wú)關(guān)的代碼已經(jīng)省略):

@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 進(jìn)來(lái)的敞斋。注釋 1 處調(diào)用 scan 方法,得到 Cursor 對(duì)象并保證它能最后關(guān)閉疾牲;2 處則是從 cursor 中取數(shù)據(jù)植捎。

上面的代碼看上去沒(méi)什么問(wèn)題,但是執(zhí)行 scanFoo0() 時(shí)會(huì)報(bào)錯(cuò):

java.lang.IllegalStateException: A Cursor is already closed.

這是因?yàn)槲覀兦懊嬲f(shuō)了在取數(shù)據(jù)的過(guò)程中需要保持?jǐn)?shù)據(jù)庫(kù)連接阳柔,而 Mapper 方法通常在執(zhí)行完后連接就關(guān)閉了焰枢,因此 Cusor 也一并關(guān)閉了。

1.2.3.1 SqlSessionFactory

我們可以用 SqlSessionFactory 來(lái)手工打開(kāi)數(shù)據(jù)庫(kù)連接舌剂,將 Controller 方法修改如下:

@Autowired
private SqlSessionFactory sqlSessionFactory;
@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 處我們開(kāi)啟了一個(gè) SqlSession (實(shí)際上也代表了一個(gè)數(shù)據(jù)庫(kù)連接),并保證它最后能關(guān)閉霍转;2 處我們使用 SqlSession 來(lái)獲得 Mapper 對(duì)象荐绝。這樣才能保證得到的 Cursor 對(duì)象是打開(kāi)狀態(tài)的。

1.2.3.2 TransactionTemplate

Spring 中避消,我們可以用 TransactionTemplate 來(lái)執(zhí)行一個(gè)數(shù)據(jù)庫(kù)事務(wù)低滩,這個(gè)過(guò)程中數(shù)據(jù)庫(kù)連接同樣是打開(kāi)的。代碼如下:

@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 處我們創(chuàng)建了一個(gè) TransactionTemplate 對(duì)象恕沫,2 處執(zhí)行數(shù)據(jù)庫(kù)事務(wù),而數(shù)據(jù)庫(kù)事務(wù)的內(nèi)容則是調(diào)用 Mapper 對(duì)象的流式查詢(xún)均驶。注意這里的 Mapper 對(duì)象無(wú)需通過(guò) SqlSession 創(chuàng)建昏兆。

1.2.3.3 @Transactional 注解

這個(gè)本質(zhì)上和方案二一樣枫虏,代碼如下:

@GetMapping("foo/scan/3/{limit}")
@Transactional
public void scanFoo3(@PathVariable("limit") int limit) throws Exception {
    try (Cursor<Foo> cursor = fooMapper.scan(limit)) {
        cursor.forEach(foo -> { });
    }
}

它僅僅是在原來(lái)方法上面加了個(gè) @Transactional 注解妇穴。這個(gè)方案看上去最簡(jiǎn)潔,但請(qǐng)注意 Spring 框架當(dāng)中注解使用的坑:只在外部調(diào)用時(shí)生效 隶债。在當(dāng)前類(lèi)中調(diào)用這個(gè)方法腾它,依舊會(huì)報(bào)錯(cuò)。
點(diǎn)擊此處了解Spring事務(wù)

1.2.4 完整示例

mybatis的所謂流式查詢(xún)死讹,就是服務(wù)端程序查詢(xún)數(shù)據(jù)的過(guò)程中瞒滴,與遠(yuǎn)程數(shù)據(jù)庫(kù)一直保持連接,不斷的去數(shù)據(jù)庫(kù)拉取數(shù)據(jù)赞警,提交事務(wù)并關(guān)閉sqlsession后妓忍,數(shù)據(jù)庫(kù)連接斷開(kāi),停止數(shù)據(jù)拉取愧旦,需要注意的是使用這種方式世剖,需要自己手動(dòng)維護(hù)sqlsession和事務(wù)的提交。

實(shí)現(xiàn)方式很簡(jiǎn)單笤虫,原來(lái)返回的類(lèi)型是集合或?qū)ο笈蕴保魇讲樵?xún)返回的的類(lèi)型Curor祖凫,泛型內(nèi)表示實(shí)際的類(lèi)型,其他沒(méi)有變化酬凳;

1.2.4.1 mapper接口和SQL

@Mapper
public interface PersonDao {
    Cursor<Person> selectByCursor();
    Integer queryCount();
 
}

對(duì)應(yīng)SQL文件

<select id="selectByCursor" resultMap="personMap">
    select * from sys_person order by id desc
</select>
<select id="queryCount" resultType="java.lang.Integer">
    select count(*) from sys_person
</select>

1.2.4.2 Service操作

dao層向service層返回的是Cursor類(lèi)型對(duì)象惠况,只要不提交關(guān)閉sqlsession,服務(wù)端程序就可以一直從數(shù)據(jù)數(shù)據(jù)庫(kù)讀取數(shù)據(jù)宁仔,直到查詢(xún)sql匹配到數(shù)據(jù)全部讀取完稠屠;

示例里的主要業(yè)務(wù)邏輯是:從sys_person表中讀取所有的人員信息數(shù)據(jù),然后按照每1000條數(shù)據(jù)為一組翎苫,讀取到內(nèi)存里進(jìn)行處理完箩,以此類(lèi)推,直到查詢(xún)sql匹配到數(shù)據(jù)全部處理完拉队,再提交事務(wù)弊知,關(guān)閉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打開(kāi)一個(gè)sqlSession粱快,在沒(méi)有讀取完數(shù)據(jù)之前不要提交事務(wù)或關(guān)閉sqlSession
                log.info("----開(kāi)啟sqlSession");
                SqlSession sqlSession = sqlSessionFactory.openSession();
                 try {
                     //獲取到指定mapper
                     PersonDao mapper = sqlSession.getMapper(PersonDao.class);
                     //調(diào)用指定mapper的方法秩彤,返回一個(gè)cursor
                     Cursor<Person> cursor = mapper.selectByCursor();
                     //查詢(xún)數(shù)據(jù)總量
                     Integer total = mapper.queryCount();
                     //定義一個(gè)list,用來(lái)從cursor中讀取數(shù)據(jù)事哭,每讀取夠1000條的時(shí)候漫雷,開(kāi)始處理這批數(shù)據(jù);
                     //當(dāng)前批數(shù)據(jù)處理完之后鳍咱,清空l(shuí)ist降盹,準(zhǔn)備接收下一批次數(shù)據(jù);直到大量的數(shù)據(jù)全部處理完谤辜;
                     List<Person> personList = new ArrayList<>();
                     int i = 0;
                     if (cursor != null) {
                         for (Person person : cursor) {
                             if (personList.size() < 1000) {
//                            log.info("----id:{},userName:{}", person.getId(), person.getUserName());
                                 personList.add(person);
                             } else if (personList.size() == 1000) {
                                 ++i;
                                 log.info("----{}蓄坏、從cursor取數(shù)據(jù)達(dá)到1000條,開(kāi)始處理數(shù)據(jù)", i);
                                 log.info("----處理數(shù)據(jù)中...");
                                 Thread.sleep(1000);//休眠1s模擬處理數(shù)據(jù)需要消耗的時(shí)間丑念;
                                 log.info("----{}涡戳、從cursor中取出的1000條數(shù)據(jù)已經(jīng)處理完畢", i);
                                 personList.clear();
                                 personList.add(person);
                             }
                             if (total == (cursor.getCurrentIndex() + 1)) {
                                 ++i;
                                 log.info("----{}、從cursor取數(shù)據(jù)達(dá)到1000條脯倚,開(kāi)始處理數(shù)據(jù)", i);
                                 log.info("----處理數(shù)據(jù)中...");
                                 Thread.sleep(1000);//休眠1s模擬處理數(shù)據(jù)需要消耗的時(shí)間渔彰;
                                 log.info("----{}、從cursor中取出的1000條數(shù)據(jù)已經(jīng)處理完畢", i);
                                 personList.clear();
                             }
                         }
                         if (cursor.isConsumed()) {
                             log.info("----查詢(xún)sql匹配中的數(shù)據(jù)已經(jīng)消費(fèi)完畢推正!");
                         }
                     }
                     sqlSession.commit();
                     log.info("----提交事務(wù)");
                 }catch (Exception e){
                     e.printStackTrace();
                     sqlSession.rollback();
                 }
                 finally {
                     if (sqlSession != null) {
                         //全部數(shù)據(jù)讀取并且做好其他業(yè)務(wù)操作之后恍涂,提交事務(wù)并關(guān)閉連接;
                         sqlSession.close();
                         log.info("----關(guān)閉sqlSession");  
                     }
                 }
                
            }
        }).start();
    }
}

1.3 游標(biāo)查詢(xún)

1.3.1 定義

對(duì)大量數(shù)據(jù)進(jìn)行處理時(shí)植榕,為防止內(nèi)存泄漏情況發(fā)生再沧,也可以采用游標(biāo)方式進(jìn)行數(shù)據(jù)查詢(xún)處理。
當(dāng)查詢(xún)百萬(wàn)級(jí)的數(shù)據(jù)的時(shí)候内贮,還可以使用游標(biāo)方式進(jìn)行數(shù)據(jù)查詢(xún)處理产园,不僅可以節(jié)省內(nèi)存的消耗汞斧,而且還不需要一次性取出所有數(shù)據(jù),可以進(jìn)行逐條處理或逐條取出部分批量處理什燕。一次查詢(xún)指定 fetchSize 的數(shù)據(jù)粘勒,直到把數(shù)據(jù)全部處理完。

1.3.2 注解查詢(xún)

Mybatis 的處理加了兩個(gè)注解:@Options@ResultType

@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
 
    // 方式一 多次獲取屎即,一次多行
    @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000000)
    Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
 
    // 方式二 一次獲取庙睡,一次一行
    @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
    @ResultType(BigDataSearchEntity.class)
    void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
 
}
  • @Options
    ResultSet.FORWORD_ONLY:結(jié)果集的游標(biāo)只能向下滾動(dòng)
    ResultSet.SCROLL_INSENSITIVE:結(jié)果集的游標(biāo)可以上下移動(dòng),當(dāng)數(shù)據(jù)庫(kù)變化時(shí)技俐,當(dāng)前結(jié)果集不變
    ResultSet.SCROLL_SENSITIVE:返回可滾動(dòng)的結(jié)果集乘陪,當(dāng)數(shù)據(jù)庫(kù)變化時(shí),當(dāng)前結(jié)果集同步改變
    fetchSize:每次獲取量
  • @ResultType
    @ResultType(BigDataSearchEntity.class):轉(zhuǎn)換成返回實(shí)體類(lèi)型

注意:返回類(lèi)型必須為 void 雕擂,因?yàn)椴樵?xún)的結(jié)果在 ResultHandler 里處理數(shù)據(jù)啡邑,所以這個(gè) hander 也是必須的,可以使用 lambda 實(shí)現(xiàn)一個(gè)依次處理邏輯井赌。

注意:雖然上面的代碼中都有 @Options 但實(shí)際操作卻有不同:
方式一是多次查詢(xún)谤逼,一次返回多條;
方式二是一次查詢(xún)仇穗,一次返回一條流部;

原因:
Oracle 是從服務(wù)器一次取出 fetch size 條記錄放在客戶端,客戶端處理完成一個(gè)批次后再向服務(wù)器取下一個(gè)批次纹坐,直到所有數(shù)據(jù)處理完成枝冀。
MySQL 是在執(zhí)行 ResultSet.next() 方法時(shí),會(huì)通過(guò)數(shù)據(jù)庫(kù)連接一條一條的返回耘子。flush buffer 的過(guò)程是阻塞式的果漾,如果網(wǎng)絡(luò)中發(fā)生了擁塞,send buffer 被填滿拴还,會(huì)導(dǎo)致 buffer 一直 flush 不出去跨晴,那 MySQL 的處理線程會(huì)阻塞欧聘,從而避免數(shù)據(jù)把客戶端內(nèi)存撐爆片林。

1.3.3 XML查詢(xún)

MyBatis實(shí)現(xiàn)逐條獲取數(shù)據(jù),必須要自定義ResultHandler怀骤,然后在mapper.xml文件中费封,對(duì)應(yīng)的select語(yǔ)句中添加 fetchSize="-2147483648"或者Integer.MIN_VALUE。最后將自定義的ResultHandler傳給SqlSession來(lái)執(zhí)行查詢(xún)蒋伦,并將返回的結(jié)果進(jìn)行處理弓摘。

注意:
fetchSize設(shè)為-2147483648(Integer.MIN_VALUE) 一開(kāi)始希望或許fetchSize能夠自己指定一次從服務(wù)器端獲取的數(shù)據(jù)量;發(fā)現(xiàn)修改fetchSize的值并沒(méi)有差別痕届;結(jié)果是MYSQL并不支持自定義fetchSize韧献,由于其他大型數(shù)據(jù)庫(kù)(oracl db2)是支持的末患;mysql使用服務(wù)器端游標(biāo)只能一條一條取數(shù)據(jù)。
如果接口方法參數(shù)沒(méi)有聲明回調(diào)函數(shù) ResultHandler 锤窑,聲明 fetchSize 也是沒(méi)有任何作用的璧针,依然會(huì)返回完整結(jié)果集

1.3.3.1 示例

以下是基于MyBatis Stream導(dǎo)出的完整的工程樣例,我們將通過(guò)對(duì)比Stream文件導(dǎo)出和傳統(tǒng)方式導(dǎo)出的內(nèi)存占用率的差異渊啰,來(lái)驗(yàn)證Stream文件導(dǎo)出的有效性探橱。

我們先定義一個(gè)工具類(lèi)DownloadProcessor,它內(nèi)部封裝一個(gè)HttpServletResponse對(duì)象绘证,用來(lái)將對(duì)象寫(xiě)入到csv隧膏。

public class DownloadProcessor {
    private final HttpServletResponse response;
     
    public DownloadProcessor(HttpServletResponse response) {
        this.response = response;
        String fileName = System.currentTimeMillis() + ".csv";
        this.response.addHeader("Content-Type", "application/csv");
        this.response.addHeader("Content-Disposition", "attachment; filename="+fileName);
        this.response.setCharacterEncoding("UTF-8");
    }
     
    public <E> void processData(E record) {
        try {
            response.getWriter().write(record.toString()); //如果是要寫(xiě)入csv,需要重寫(xiě)toString,屬性通過(guò)","分割
            response.getWriter().write("\n");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

然后通過(guò)實(shí)現(xiàn) org.apache.ibatis.session.ResultHandler,自定義我們的ResultHandler嚷那,它用于獲取java對(duì)象胞枕,然后傳遞給上面的DownloadProcessor處理類(lèi)進(jìn)行寫(xiě)文件操作:

public class CustomResultHandler implements ResultHandler {

    private final DownloadProcessor downloadProcessor;
     
    public CustomResultHandler(
            DownloadProcessor downloadProcessor) {
        super();
        this.downloadProcessor = downloadProcessor;
    }
     
    @Override
    public void handleResult(ResultContext resultContext) {
        Authors authors = (Authors)resultContext.getResultObject();
        downloadProcessor.processData(authors);
    }
}
實(shí)體類(lèi):
@Data
public class Authors {
    private Integer id;
    private String firstName;
     
    private String lastName;
     
    private String email;
     
    private Date birthdate;
     
    private Date added;
     
   
}
Mapper接口:

public interface AuthorsMapper {
   List<Authors> selectByExample(AuthorsExample example);
    
   List<Authors> streamByExample(AuthorsExample example); //以stream形式從mysql獲取數(shù)據(jù)
}

Mapper xml文件核心片段,以下兩條select的唯一差異就是在stream獲取數(shù)據(jù)的方式中多了一條屬性:fetchSize="-2147483648"

<select id="selectByExample" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
    select
    <if test="distinct">
      distinct
    </if>
    'false' as QUERYID,
    <include refid="Base_Column_List" />
    from authors
    <if test="_parameter != null">
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null">
      order by ${orderByClause}
    </if>
  </select>
  <select id="streamByExample" fetchSize="-2147483648" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
    select
    <if test="distinct">
      distinct
    </if>
    'false' as QUERYID,
    <include refid="Base_Column_List" />
    from authors
    <if test="_parameter != null">
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null">
      order by ${orderByClause}
    </if>
  </select>

獲取數(shù)據(jù)的核心service如下魏宽,由于只做個(gè)簡(jiǎn)單演示曲稼,就懶得寫(xiě)成接口了。其中 streamDownload 方法即為stream取數(shù)據(jù)寫(xiě)文件的實(shí)現(xiàn)湖员,它將以很低的內(nèi)存占用從MySQL獲取數(shù)據(jù)贫悄;此外還提供traditionDownload方法,它是一種傳統(tǒng)的下載方式娘摔,批量獲取全部數(shù)據(jù)窄坦,然后將每個(gè)對(duì)象寫(xiě)入文件。

@Service
public class AuthorsService {
    private final SqlSessionTemplate sqlSessionTemplate;
    private final AuthorsMapper authorsMapper;

    public AuthorsService(SqlSessionTemplate sqlSessionTemplate, AuthorsMapper authorsMapper) {
        this.sqlSessionTemplate = sqlSessionTemplate;
        this.authorsMapper = authorsMapper;
    }

    /**
     * stream讀數(shù)據(jù)寫(xiě)文件方式
     * @param httpServletResponse
     * @throws IOException
     */
    public void streamDownload(HttpServletResponse httpServletResponse)
            throws IOException {
        AuthorsExample authorsExample = new AuthorsExample();
        authorsExample.createCriteria();
        HashMap<String, Object> param = new HashMap<>();
        param.put("oredCriteria", authorsExample.getOredCriteria());
        param.put("orderByClause", authorsExample.getOrderByClause());
        CustomResultHandler customResultHandler = new CustomResultHandler(new DownloadProcessor (httpServletResponse));
        sqlSessionTemplate.select(
                "com.alphathur.mysqlstreamingexport.mapper.AuthorsMapper.streamByExample", param, customResultHandler);
        httpServletResponse.getWriter().flush();
        httpServletResponse.getWriter().close();
    }

    /**
     * 傳統(tǒng)下載方式
     * @param httpServletResponse
     * @throws IOException
     */
    public void traditionDownload(HttpServletResponse httpServletResponse)
            throws IOException {
        AuthorsExample authorsExample = new AuthorsExample();
        authorsExample.createCriteria();
        List<Authors> authors = authorsMapper.selectByExample (authorsExample);
        DownloadProcessor downloadProcessor = new DownloadProcessor (httpServletResponse);
        authors.forEach (downloadProcessor::processData);
        httpServletResponse.getWriter().flush();
        httpServletResponse.getWriter().close();
    }
}
下載的入口controller:

@RestController
@RequestMapping("download")
public class HelloController {
    private final AuthorsService authorsService;

    public HelloController(AuthorsService authorsService) {
        this.authorsService = authorsService;
    }

    @GetMapping("streamDownload")
    public void streamDownload(HttpServletResponse response)
            throws IOException {
        authorsService.streamDownload(response);
    }

    @GetMapping("traditionDownload")
    public void traditionDownload(HttpServletResponse response)
            throws IOException {
        authorsService.traditionDownload (response);
    }
}  
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凳寺,一起剝皮案震驚了整個(gè)濱河市鸭津,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肠缨,老刑警劉巖逆趋,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異晒奕,居然都是意外死亡闻书,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)脑慧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)魄眉,“玉大人,你說(shuō)我怎么就攤上這事闷袒】勇桑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵囊骤,是天一觀的道長(zhǎng)晃择。 經(jīng)常有香客問(wèn)我冀值,道長(zhǎng),這世上最難降的妖魔是什么宫屠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任池摧,我火速辦了婚禮,結(jié)果婚禮上激况,老公的妹妹穿的比我還像新娘作彤。我一直安慰自己,他們只是感情好乌逐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布竭讳。 她就那樣靜靜地躺著,像睡著了一般浙踢。 火紅的嫁衣襯著肌膚如雪绢慢。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天洛波,我揣著相機(jī)與錄音胰舆,去河邊找鬼。 笑死蹬挤,一個(gè)胖子當(dāng)著我的面吹牛缚窿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焰扳,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼倦零,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了吨悍?” 一聲冷哼從身側(cè)響起扫茅,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎育瓜,沒(méi)想到半個(gè)月后葫隙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躏仇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年恋脚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钙态。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慧起,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出册倒,到底是詐尸還是另有隱情,我是刑警寧澤磺送,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布驻子,位于F島的核電站灿意,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崇呵。R本人自食惡果不足惜缤剧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望域慷。 院中可真熱鬧荒辕,春花似錦、人聲如沸犹褒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叠骑。三九已至李皇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宙枷,已是汗流浹背掉房。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慰丛,地道東北人卓囚。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诅病,于是被迫代替她去往敵國(guó)和親捍岳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容