【カスタマイズ例】大容量テーブルのCSV出力!

こんにちは!Aipo工房です!

うだるような暑さが続いておりますが、みなさまはいかがお過ごしでしょうか?

熱中症にならないように予防はしっかりとしておきたいですね

さて、タイトルの通りにある案件でスケジュールとほぼ同数のレコードをもつテーブルから

CSVを作成することになりました。(Aipoのバージョンは5です)

Aipoには既にDBのテーブルをCSVに出力するための機能がクラス化されていますので

それを参考に実装すれば「そんなに大変じゃないかなぁ」とたかをくくっていたのですが・・・

①10万件近くのレコードをCSVに出力しようとするとOutOfMemoryErrorが!

特にヒープサイズが不足していることはなかったのですが、調べてみると

以下のメソッド中で発生していました

List<?> alist = dataContext.performQuery(query);

これはcayenneオブジェクトのメソッドですが指定したquery(オブジェクトですがSql文みたいなものです)でDBから抽出したデータを全てリストに格納しています。

stringbuilderでOutOfMemoryErrorが発生していましたので処理内の文字列の扱いに何かしら問題があるのかもしれません

②CSV書き込み時の問題

エラーとは直接関係がないのですが、CSVを出力するクラスのスーパークラス(ALCSVScreen)で以下処理にもちょっと問題がありそうです

@Override
protected void doOutput(RunData rundata) throws Exception {
ServletOutputStream out = null;
try {
String result = getCSVString(rundata);
String fileName = getFileName();
HttpServletResponse response = rundata.getResponse();
// ファイル名の送信
response.setHeader(“Content-disposition”, “attachment; filename=\””
+ fileName
+ “\””);
response.setHeader(“Cache-Control”, “aipo”);
response.setHeader(“Pragma”, “aipo”);
// ファイル内容の出力
out = response.getOutputStream();
out.write(result.getBytes(csvFileEncoding));
out.flush();
out.close();
} catch (Exception e) {
logger.error(“ALCSVScreen.doOutput”, e);
}
}

getCSVString()は抽象メソッドで継承先で実装します。

具体的にはgetCSVString内でCSVに出力する内容をすべて取得して文字列でリターンするようにします。

doOutputではその文字列をレスポンスのストリームに書き込んでクライアントに返却するようになっています。

つまりCSVに書き込む文字列がすべて一旦メモリ上に保持されることになります。

数十万、数百万のレコードを処理することになるとここも危ないです。

 

①と②の問題両方回避するため、以下ように変更しました。

1.doOutputメソッドは継承先でオーバーライドします。

2.doOutput内を以下の通り実装します。

2-1 CSVヘッダー部分をレスポンスに書き込みします。

2-2 フェッチ用のcayenneオブジェクトでDBからデータを取得します。

2-2 ,2-3で取得したDBデータはそのままレスポンスに書き込みます。

 

以下はサンプルのコードになります。

protected void doOutput(RunData rundata) throws Exception {
ServletOutputStream out = null;
try {
//データコンテキストを取得
DataContext dataContext = DatabaseOrmService.getInstance().getDataContext();
String fileName = getFileName();
//ヘッダー定義
HttpServletResponse response = rundata.getResponse();
// ファイル名の送信
response.setHeader(“Content-disposition”, “attachment; filename=\””
+ fileName + “\””);
response.setHeader(“Cache-Control”, “aipo”);
response.setHeader(“Pragma”, “aipo”);
// ファイル内容の出力
out = response.getOutputStream();
String LINE_SEPARATOR =System.getProperty(“line.separator”);
StringBuffer sbh = new StringBuffer();
// 1行目にヘッダー情報を挿入
sbh.append(“ヘッダー1,ヘッダー2,ヘッダー3”);
sbh.append(LINE_SEPARATOR);
out.write(sbh.toString().getBytes(csvFileEncoding));
SQLTemplate query = null;
Map<String, Object> parameterList = null;
String queryString = StringUtils.EMPTY;
//レコード取得用のqueryを作成する
parameterList = new HashMap<String, Object>();
queryString = this.getSelectQueryString(rundata, parameterList);
query = new SQLTemplate(EipTActual.class, queryString);
query.setParameters(parameterList);
StringBuffer sb = new StringBuffer();
//フェッチ用のcayenneオブジェクトでDBからデータを取得
ResultIterator it = null;
it = dataContext.performIteratedQuery(query);
try{
 while(it.hasNextRow()) {
Map map = it.nextDataRow();
 //mapの内容をstringbuilderに書き込む
//レスポンスに書き込んでstringbuilderをクリア
out.write(sb.toString().getBytes(csvFileEncoding));
sb.setLength(0);
}
}finally{
it.close();
}out.flush();
out.close();
} catch (Exception e) {
logger.error(“[ERROR]”, e);
}
}

とりあえずは10万件のレコードをCSV出力することができました!