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.Closeable
和 java.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);
}
}