導入と目的
本稿では、業務システムにおけるテーブルデータのインポート・エクスポート機能の実装方法について、Alibaba EasyExcelを利用した具体的な実装パターンと設計上の注意点を紹介します。特に、大量データ処理時のメモリ効率やエラー処理の設計、フロントエンドとの連携における制約に焦点を当てます。
主要ライブラリ:EasyExcelの概要
EasyExcelは、大容量のXLSXファイルを低メモリで読み書き可能なJavaライブラリです。3.0.2以降では、64MBのメモリ環境でも75MB(46万行×25列)のExcelファイルを20秒以内に読み込み可能です。
// 簡単な読み取り例
String fileName = "data.xlsx";
EasyExcel.read(fileName, UserData.class, new UserReadListener()).sheet().doRead();
// 簡単な書き出し例
String outputPath = "output_" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(outputPath, UserData.class).sheet("ユーザー一覧").doWrite(userList);
エクスポート機能の実装
1. 単一シート出力
HTTPレスポンスのOutputStreamに直接書き込むことで、ダウンロード形式の出力を実現します。コンテンツタイプと文字エンコーディングの設定が重要です。
@RequestMapping("/export/single")
public void exportSingle(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String filename = URLEncoder.encode("ユーザー情報", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
EasyExcel.write(response.getOutputStream(), UserDTO.class)
.inMemory(true)
.sheet("ユーザー一覧")
.doWrite(userService.findAll());
}
2. 複数シート出力
複数シートを含むファイルを作成するには、ExcelWriterクラスを使用し、各シートごとにWriteSheetを定義します。
@RequestMapping("/export/multi")
public void exportMulti(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String filename = URLEncoder.encode("統合レポート", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream()).inMemory(true).build()) {
WriteSheet userSheet = EasyExcel.writerSheet(0, "ユーザー情報").head(UserDTO.class).build();
writer.write(userService.findAll(), userSheet);
WriteSheet citySheet = EasyExcel.writerSheet(1, "都市マスタ").head(CityDTO.class).build();
writer.write(cityService.findAll(), citySheet);
WriteSheet companySheet = EasyExcel.writerSheet(2, "企業情報").head(CompanyDTO.class).build();
writer.write(companyService.findAll(), companySheet);
}
}
インポート機能の実装
1. リスナーによる逐次処理
大規模なファイルを扱う場合、すべてのデータをメモリに保持するとオブジェクトメモリ不足(OOM)リスクが高まります。そのため、AnalysisEventListenerを継承して、バッチ単位でデータを処理する設計が推奨されます。
public class ImportListener extends AnalysisEventListener<UserData> {
private final List<UserData> batchData = new ArrayList<>(100);
private final List<String> errors = new ArrayList<>();
@Override
public void invoke(UserData data, AnalysisContext context) {
if (validate(data)) {
batchData.add(data);
} else {
errors.add("行" + (context.readRowHolder().getRowIndex() + 1) + "に不正データがあります");
}
if (batchData.size() >= 100) {
saveBatch(batchData);
batchData.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!batchData.isEmpty()) {
saveBatch(batchData);
}
}
private boolean validate(UserData data) {
return data.getUserName() != null && !data.getUserName().trim().isEmpty()
&& data.getUserPhone() != null && data.getUserPhone().matches("^1[0-9]{10}$");
}
}
2. エラーハンドリングと制限事項
- 最大行数制限:1回のアップロードで2,000行を超える場合は例外を発生させる
- セル値の空チェックと正規表現によるバリデーション
- Spring管理対象外のリスナークラス(毎回newが必要)
重要なアノテーションの使い方
| アノテーション | 用途 |
|---|---|
@ExcelProperty | ヘッダー名の指定および複数行ヘッダー(ネスト)に対応 |
@ColumnWidth | 列幅の固定(単位:文字数) |
@ExcelIgnore | 出力対象から除外するフィールドの指定 |
@ExcelProperty(value = {"基本情報", "電話番号"}, index = 2)
@ColumnWidth(20)
private String phone;
設計上のポイントまとめ
- 大量データの処理では、
inMemory(true)を使ってメモリ上での一時保存を回避 - Web出力時は
response.getOutputStream()を直接使用し、自動クローズを活用 - リスナーはステートレス設計が望ましく、DIコンテナ管理を避ける
- エラーメッセージの集約と返却により、ユーザーに明確なフィードバックを提供
- シート数が多すぎる場合は、分割ダウンロードを検討