0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ファイル操作のまとめ

0
Last updated at Posted at 2026-06-09

はじめに

DBから取得した内容をファイルで出力したいことがありました。
エクセル形式で出力する場合、どのようなことを考慮する必要があるかをまとめます。

参考記事

全体構成

  1. Controller でリクエストを受ける
  2. リクエストから検索条件と出力形式(ファイル種別・文字コード)を取り出す
  3. ファイル出力処理のメソッドを呼び出す
  4. DAO の SQL を実行し結果を dto へ変換、出力形式に応じて Apache POI などでファイルを生成する
  5. 生成したファイルを controller へ返し、クライアントへのダウンロード処理に渡す
    この構成が成立するのは、controller が生成ファイル(またはストリーム)を自由にクライアントへ返せる場合である。

フレームワークが特定の戻り型を強制する作りでないこと
エクスポート実行経路で実際に呼ばれるメソッドにだけ自作の private 処理を差し込み、戻り値として独自 Response 型を返す 。それ以外のオーバーライドメソッドを安易に return null で潰すと、フレームワークがその戻り値を参照していた場合に NullPointerException で落ちる。

判断は次の通り。

  • 本体処理を差し込むメソッド … private を呼び、生成物を独自 Response 型として返す
  • 本体処理を差し込まないメソッド … 戻り値が参照されるか確認する。参照されるなら空オブジェクト等を返す。参照されないなら return null でよい

エンコード方式の扱い

出力形式として「ファイル種別」と「エンコード方式」が渡される。ただし エンコード方式が意味を持つのは CSV や TSV などのテキスト出力のときだけ である。xlsx は内部が ZIP と XML(UTF-8 固定)で構成され、POI が内部で encoding を処理するため、エンコード方式のパラメータは xlsx 出力では使われない。

したがってファイル種別で分岐し、エンコード方式は CSV 分岐でのみ使う、という整理が正しい。xlsx 固定ならエンコード方式は無視してよい。

Controller

controller は検索条件を取り出し、ロジッククラスのオーバーライドメソッドを呼び、戻ってきた Response を返すだけにする。

public class DownloadController {
 
    public ExportResponse output(ExportRequest request) {
        // 画面から渡された検索条件を取り出す(新規/更新モード含む想定)
        SearchCondition condition = request.getSearchCondition();
 
        ExcelExportService service = new ExcelExportService();
        // AbstractExporter のオーバーライドメソッドを呼ぶ(戻り値は独自 Response 型)
        return service.export(condition);
    }
}

ExportResponse はフレームワーク独自の Response 型に読み替えること。

ExcelExportService

AbstractExporter を継承し、実行経路上のメソッドをオーバーライドする。その中で private メソッドを順に呼び、最後に独自 Response 型を返す。

public class ExcelExportService extends AbstractExporter {
 
    /** 最大出力行(データ行) */
    private static final int MAX_OUTPUT_ROWS = 100;
 
    // AbstractExporter のメソッドをオーバーライド(メソッド名・シグネチャは実際の定義に合わせる)
    @Override
    public ExportResponse export(SearchCondition condition) {
        List<RecordModel> models = executeSearch(condition); // 1. DAO 実行
        List<RecordDto> dtoList = toDtoList(models);          // 2. dto 変換
 
        // 最大100行の制約チェック
        if (dtoList.size() > MAX_OUTPUT_ROWS) {
            throw new IllegalStateException(
                "出力行が上限を超えています: " + dtoList.size() + " > " + MAX_OUTPUT_ROWS);
        }
 
        File file = writeXlsx(dtoList); // 3. xlsx 出力
        return buildResponse(file);     // 4. 独自 Response 型に詰めて返す
    }
 
    // 実行経路に無いオーバーライドメソッドは戻り値参照に注意して潰す
    // 参照されないことを Call Hierarchy で確認してから return null とする
    @Override
    public SomeType unusedMethod() {
        return null;
    }
}

1. 検索(検索条件・モードで SQL を出し分け)

既存 DAO をそのまま使い、モードによって呼び出す SQL メソッドを分岐させる。新規/更新モードは検索条件に含める想定とする。

private List<RecordModel> executeSearch(SearchCondition condition) {
    RecordDao dao = getDao(); // 既存 DAO の取得
    if (condition.isNewMode()) {
        return dao.findForNew(condition);
    } else {
        return dao.findForUpdate(condition);
    }
}

2. model から dto への変換

DAO は model のリストを返すため、画面出力したい dto へ詰め替える。手動マッピングが最も確実で速い。dto には 出力する項目だけ 定義すればよい。

private List<RecordDto> toDtoList(List<RecordModel> models) {
    List<RecordDto> dtoList = new ArrayList<RecordDto>();
    for (RecordModel m : models) {
        RecordDto dto = new RecordDto();
        dto.setShohinCode(m.getShohinCode());
        dto.setShohinName(m.getShohinName());
        // 出力する項目分だけ繰り返す
        dtoList.add(dto);
    }
    return dtoList;
}

3. 列定義(ヘッダー・表示形式・対象区分を一元管理)

ヘッダーが10列以上になると、ヘッダー名の配列と値の配列を別々に持つ方式では、列の追加・削除・並び替えのたびに複数箇所を直す必要があり、ずれによる不具合が起きやすい。

実務では 1列分の情報(ヘッダー名・dto からの値の取り出し・表示形式・対象/対象外区分)を1箇所にまとめた列定義 を用意し、それを並べたリストで管理するのが扱いやすい。列を追加するときは定義を1ブロック足すだけで済み、ヘッダー出力・値出力・スタイル適用がすべて同じ定義を参照する。

旧 Java(ラムダ非対応)を想定し、値の取り出しは匿名クラスで列ごとに持たせる。

/** 表示形式の種別 */
private enum CellFormat { STANDARD, TEXT }
 
/** 1列分の定義 */
private static abstract class ColumnDef {
    final String header;      // ヘッダー名
    final CellFormat format;  // 表示形式
    final boolean excluded;   // 対象外(グレー)スタイルか
 
    ColumnDef(String header, CellFormat format, boolean excluded) {
        this.header = header;
        this.format = format;
        this.excluded = excluded;
    }
 
    /** dto から、この列に出力する値を取り出す */
    abstract Object getValue(RecordDto dto);
}
 
/** 列定義一覧(10列以上を想定。1列=1ブロックで追加・削除が容易) */
private static final List<ColumnDef> COLUMNS = buildColumns();
 
private static List<ColumnDef> buildColumns() {
    List<ColumnDef> list = new ArrayList<ColumnDef>();
 
    // 商品コードは先頭ゼロ保持のため文字列形式
    list.add(new ColumnDef("商品コード", CellFormat.TEXT, false) {
        Object getValue(RecordDto dto) { return dto.getShohinCode(); }
    });
    list.add(new ColumnDef("商品名", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getShohinName(); }
    });
    list.add(new ColumnDef("数量", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getSuryo(); }
    });
    list.add(new ColumnDef("単価", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getTanka(); }
    });
    list.add(new ColumnDef("金額", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getKingaku(); }
    });
    list.add(new ColumnDef("取引先コード", CellFormat.TEXT, false) {
        Object getValue(RecordDto dto) { return dto.getTorihikiCode(); }
    });
    list.add(new ColumnDef("取引先名", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getTorihikiName(); }
    });
    list.add(new ColumnDef("担当者", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getTantosha(); }
    });
    list.add(new ColumnDef("登録日", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getTorokubi(); }
    });
    list.add(new ColumnDef("更新日", CellFormat.STANDARD, false) {
        Object getValue(RecordDto dto) { return dto.getKoshinbi(); }
    });
    // 備考・内部管理番号は対象外(グレー表示)
    list.add(new ColumnDef("備考", CellFormat.STANDARD, true) {
        Object getValue(RecordDto dto) { return dto.getBiko(); }
    });
    list.add(new ColumnDef("内部管理番号", CellFormat.TEXT, true) {
        Object getValue(RecordDto dto) { return dto.getKanriNo(); }
    });
 
    return list;
}

4. ヘッダー出力

列定義のリストを順に回してヘッダー行を作る。10列でも100列でも、ヘッダー出力のコードは変えなくてよい。実務ではヘッダーを目立たせる太字スタイルと、スクロール時もヘッダーが残る固定(freeze)を入れておくと使いやすい。

private void writeHeader(Workbook workbook, Sheet sheet) {
    CellStyle headerStyle = createHeaderStyle(workbook);
 
    Row headerRow = sheet.createRow(0);
    for (int j = 0; j < COLUMNS.size(); j++) {
        Cell cell = headerRow.createCell(j);
        cell.setCellValue(COLUMNS.get(j).header);
        cell.setCellStyle(headerStyle);
    }
 
    // ヘッダー行を固定(1行目を常時表示)
    sheet.createFreezePane(0, 1);
}
 
private CellStyle createHeaderStyle(Workbook workbook) {
    CellStyle style = workbook.createCellStyle();
    Font font = workbook.createFont();
    font.setBoldweight(Font.BOLDWEIGHT_BOLD);
    style.setFont(font);
    style.setAlignment(CellStyle.ALIGN_CENTER);
    return style;
}

5. スタイルの事前生成(表示形式・対象区分を含む)

スタイルは 事前に生成して使い回す 。POI はスタイル数に上限があるため、行・セルごとに createCellStyle を呼ぶのは避ける。

ここでは表示形式(標準・文字列)と対象区分(対象・対象外)の組み合わせごとに1つずつスタイルを作り、Map にキャッシュする。文字列形式は組み込みフォーマット @ を指定する。文字列形式にすると、先頭ゼロやコード値が数値として丸められず、入力した文字列のまま表示される。

private Map<String, CellStyle> buildStyleCache(Workbook workbook) {
    Map<String, CellStyle> cache = new HashMap<String, CellStyle>();
    short textFormat = (short) BuiltinFormats.getBuiltinFormat("@");
 
    for (CellFormat fmt : CellFormat.values()) {
        for (int e = 0; e <= 1; e++) {
            boolean excluded = (e == 1);
 
            CellStyle style = workbook.createCellStyle();
 
            // 寄せ(対象外は右寄せ、対象は左寄せ)
            style.setAlignment(excluded ? CellStyle.ALIGN_RIGHT : CellStyle.ALIGN_LEFT);
 
            // 対象外は背景グレー
            if (excluded) {
                style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
                style.setFillPattern(CellStyle.SOLID_FOREGROUND);
            }
 
            // 表示形式(文字列のときだけ @ を設定)
            if (fmt == CellFormat.TEXT) {
                style.setDataFormat(textFormat);
            }
 
            cache.put(styleKey(fmt, excluded), style);
        }
    }
    return cache;
}
 
private String styleKey(CellFormat format, boolean excluded) {
    return format.name() + "-" + excluded;
}

6. データ行の書き込み

列定義とスタイルキャッシュを使い、各セルに値と「表示形式・対象区分に応じたスタイル」を当てる。出力は最大100行に収まる。

private File writeXlsx(List<RecordDto> dtoList) {
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet("明細");
 
    writeHeader(workbook, sheet);
 
    // スタイルを事前生成して使い回す
    Map<String, CellStyle> styleCache = buildStyleCache(workbook);
 
    int rowIndex = 1; // ヘッダーの次の行から
    for (RecordDto dto : dtoList) {
        Row row = sheet.createRow(rowIndex++);
 
        for (int j = 0; j < COLUMNS.size(); j++) {
            ColumnDef col = COLUMNS.get(j);
            Cell cell = row.createCell(j);
 
            // 値を表示形式に合わせて設定
            setCellValue(cell, col.getValue(dto), col.format);
 
            // 表示形式・対象区分に対応する使い回しスタイルを当てる
            CellStyle style = styleCache.get(styleKey(col.format, col.excluded));
            cell.setCellStyle(style);
        }
    }
 
    File tempFile;
    FileOutputStream fos = null;
    try {
        tempFile = File.createTempFile("record_export_", ".xlsx");
        fos = new FileOutputStream(tempFile);
        workbook.write(fos);
    } catch (IOException e) {
        throw new RuntimeException("xlsx の書き込みに失敗しました", e);
    } finally {
        try { if (fos != null) fos.close(); } catch (IOException ignore) {}
        try { workbook.close(); } catch (IOException ignore) {} // dispose は不要
    }
    return tempFile;
}
 
// 値を表示形式に合わせてセルに設定する
private void setCellValue(Cell cell, Object value, CellFormat format) {
    if (value == null) {
        cell.setCellValue("");
        return;
    }
    if (format == CellFormat.TEXT) {
        // 文字列形式(先頭ゼロやコード値をそのまま保持)
        cell.setCellValue(value.toString());
    } else if (value instanceof Number) {
        cell.setCellValue(((Number) value).doubleValue());
    } else {
        cell.setCellValue(value.toString());
    }
}
別手段3〜8

3. 列定義(dto フィールド名・ヘッダー名・表示書式)

ヘッダーが10列以上になると、ヘッダー名の配列と値の配列を別々に持つ方式では、列の追加・削除・並び替えのたびに複数箇所を直す必要があり、ずれによる不具合が起きやすい。

そこで 1列分の情報(dto のフィールド名・ヘッダー名・表示書式)を1つの定義にまとめ 、それを並べたリストで管理する。これは intra-mart 独自ライブラリの Tuple(フィールド名・呼称・型)と同じ並びである。

表示書式は、Excel の「セルの書式設定」の「表示形式」に対応する。文字列・数値・日付などを列ごとに指定できるよう、CellFormat を enum で定義する。

/** 表示書式の種別(Excel の「表示形式」に対応) */
private enum CellFormat { TEXT, INTEGER, DECIMAL, DATE }
 
/** 1列分の定義(dto フィールド名・ヘッダー名・表示書式) */
private static class ColumnDef {
    final String fieldName;   // dto のフィールド名
    final String header;      // ヘッダー名
    final CellFormat format;  // 表示書式
 
    ColumnDef(String fieldName, String header, CellFormat format) {
        this.fieldName = fieldName;
        this.header = header;
        this.format = format;
    }
}

列定義一覧は、1列1行で並べる。Tuple と同じく「フィールド名・ヘッダー名・表示書式」の順で書く。列を追加するときはこのリストに1行足し、後述の getValue にも対応する分岐を1行足す。

/** 列定義一覧(10列以上を想定) */
private static final List<ColumnDef> COLUMNS = buildColumns();
 
private static List<ColumnDef> buildColumns() {
    List<ColumnDef> list = new ArrayList<ColumnDef>();
    // フィールド名,            ヘッダー名,      表示書式
    list.add(new ColumnDef("shohinCode",   "商品コード",   CellFormat.TEXT));
    list.add(new ColumnDef("shohinName",   "商品名",       CellFormat.TEXT));
    list.add(new ColumnDef("suryo",        "数量",         CellFormat.INTEGER));
    list.add(new ColumnDef("tanka",        "単価",         CellFormat.INTEGER));
    list.add(new ColumnDef("kingaku",      "金額",         CellFormat.INTEGER));
    list.add(new ColumnDef("zeiritsu",     "消費税率",     CellFormat.DECIMAL));
    list.add(new ColumnDef("torihikiCode", "取引先コード", CellFormat.TEXT));
    list.add(new ColumnDef("torihikiName", "取引先名",     CellFormat.TEXT));
    list.add(new ColumnDef("tantosha",     "担当者",       CellFormat.TEXT));
    list.add(new ColumnDef("torokubi",     "登録日",       CellFormat.DATE));
    list.add(new ColumnDef("koshinbi",     "更新日",       CellFormat.DATE));
    list.add(new ColumnDef("biko",         "備考",         CellFormat.TEXT));
    list.add(new ColumnDef("kanriNo",      "内部管理番号", CellFormat.TEXT));
    return list;
}

4. フィールド名から値を取り出す

フィールド名(文字列)に対応する dto の値を取り出す。メソッド参照を使わず、if 分岐で明示的に書く。列定義に1行足したら、ここにも1行足す。

private Object getValue(RecordDto dto, String fieldName) {
    if ("shohinCode".equals(fieldName))   return dto.getShohinCode();
    if ("shohinName".equals(fieldName))   return dto.getShohinName();
    if ("suryo".equals(fieldName))        return dto.getSuryo();
    if ("tanka".equals(fieldName))        return dto.getTanka();
    if ("kingaku".equals(fieldName))      return dto.getKingaku();
    if ("zeiritsu".equals(fieldName))     return dto.getZeiritsu();
    if ("torihikiCode".equals(fieldName)) return dto.getTorihikiCode();
    if ("torihikiName".equals(fieldName)) return dto.getTorihikiName();
    if ("tantosha".equals(fieldName))     return dto.getTantosha();
    if ("torokubi".equals(fieldName))     return dto.getTorokubi();
    if ("koshinbi".equals(fieldName))     return dto.getKoshinbi();
    if ("biko".equals(fieldName))         return dto.getBiko();
    if ("kanriNo".equals(fieldName))      return dto.getKanriNo();
    throw new IllegalArgumentException("未定義のフィールド名: " + fieldName);
}

5. 対象外の列を別リストで管理

対象外(背景グレー)にする列は、列定義とは別のリストで管理する。ここではヘッダー名で指定する。

/** 対象外(グレー)にする列のヘッダー名 */
private static final List<String> EXCLUDED_HEADERS = buildExcludedHeaders();
 
private static List<String> buildExcludedHeaders() {
    List<String> list = new ArrayList<String>();
    list.add("備考");
    list.add("内部管理番号");
    return list;
}
 
private boolean isExcluded(String header) {
    return EXCLUDED_HEADERS.contains(header);
}

6. スタイルの事前生成

スタイルは 事前に生成して使い回す 。POI はスタイル数に上限があるため、行・セルごとに createCellStyle を呼ぶのは避ける。

ここでは2種類を用意する。

  • 表示書式ごとのスタイル(文字列・整数・小数・日付)。対象列に使う
  • 対象外用のスタイル(背景グレー・文字列形式)。対象外列に使う。事前に1つだけ作っておく
// 表示書式ごとのスタイル(対象列に使う)
private Map<CellFormat, CellStyle> buildFormatStyles(Workbook workbook) {
    Map<CellFormat, CellStyle> cache = new HashMap<CellFormat, CellStyle>();
    for (CellFormat fmt : CellFormat.values()) {
        CellStyle style = workbook.createCellStyle();
        style.setAlignment(CellStyle.ALIGN_LEFT);
        style.setDataFormat(resolveDataFormat(workbook, fmt));
        cache.put(fmt, style);
    }
    return cache;
}
 
// 対象外用のスタイル(背景グレー・文字列形式)を1つだけ作る
private CellStyle createExcludedStyle(Workbook workbook) {
    CellStyle style = workbook.createCellStyle();
    style.setAlignment(CellStyle.ALIGN_LEFT);
    style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
    style.setFillPattern(CellStyle.SOLID_FOREGROUND);
    style.setDataFormat((short) BuiltinFormats.getBuiltinFormat("@")); // 文字列
    return style;
}
 
// 表示書式の種別を Excel の書式文字列に変換する
private short resolveDataFormat(Workbook workbook, CellFormat format) {
    DataFormat df = workbook.createDataFormat();
    if (format == CellFormat.TEXT)    return df.getFormat("@");
    if (format == CellFormat.INTEGER) return df.getFormat("#,##0");
    if (format == CellFormat.DECIMAL) return df.getFormat("#,##0.00");
    if (format == CellFormat.DATE)    return df.getFormat("yyyy/mm/dd");
    return df.getFormat("General");
}

7. ヘッダー出力

列定義のリストを順に回してヘッダー行を作る。10列でも100列でも、ヘッダー出力のコードは変えなくてよい。実務ではヘッダーを目立たせる太字スタイルと、スクロール時もヘッダーが残る固定(freeze)を入れておくと使いやすい。

private void writeHeader(Workbook workbook, Sheet sheet) {
    CellStyle headerStyle = createHeaderStyle(workbook);
 
    Row headerRow = sheet.createRow(0);
    for (int j = 0; j < COLUMNS.size(); j++) {
        Cell cell = headerRow.createCell(j);
        cell.setCellValue(COLUMNS.get(j).header);
        cell.setCellStyle(headerStyle);
    }
 
    // ヘッダー行を固定(1行目を常時表示)
    sheet.createFreezePane(0, 1);
}
 
private CellStyle createHeaderStyle(Workbook workbook) {
    CellStyle style = workbook.createCellStyle();
    Font font = workbook.createFont();
    font.setBoldweight(Font.BOLDWEIGHT_BOLD);
    style.setFont(font);
    style.setAlignment(CellStyle.ALIGN_CENTER);
    return style;
}

8. データ行の書き込み

列定義を順に処理し、対象外の列なら 事前に作っておいたグレー+文字列のスタイル に差し替え、値も文字列として出力する。対象の列は表示書式に対応したスタイルを当てる。出力は最大100行に収まる。

private File writeXlsx(List<RecordDto> dtoList) {
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet("明細");
 
    writeHeader(workbook, sheet);
 
    // スタイルを事前生成して使い回す
    Map<CellFormat, CellStyle> formatStyles = buildFormatStyles(workbook);
    CellStyle excludedStyle = createExcludedStyle(workbook);
 
    int rowIndex = 1; // ヘッダーの次の行から
    for (RecordDto dto : dtoList) {
        Row row = sheet.createRow(rowIndex++);
 
        for (int j = 0; j < COLUMNS.size(); j++) {
            ColumnDef col = COLUMNS.get(j);
            Cell cell = row.createCell(j);
 
            Object value = getValue(dto, col.fieldName);
 
            if (isExcluded(col.header)) {
                // 対象外 … グレー+文字列スタイル、値も文字列で出力
                setCellValue(cell, value, CellFormat.TEXT);
                cell.setCellStyle(excludedStyle);
            } else {
                // 対象 … 表示書式に対応したスタイル
                setCellValue(cell, value, col.format);
                cell.setCellStyle(formatStyles.get(col.format));
            }
        }
    }
 
    File tempFile;
    FileOutputStream fos = null;
    try {
        tempFile = File.createTempFile("record_export_", ".xlsx");
        fos = new FileOutputStream(tempFile);
        workbook.write(fos);
    } catch (IOException e) {
        throw new RuntimeException("xlsx の書き込みに失敗しました", e);
    } finally {
        try { if (fos != null) fos.close(); } catch (IOException ignore) {}
        try { workbook.close(); } catch (IOException ignore) {} // dispose は不要
    }
    return tempFile;
}
 
// 値を表示書式に合わせてセルに設定する
private void setCellValue(Cell cell, Object value, CellFormat format) {
    if (value == null) {
        cell.setCellValue("");
        return;
    }
    if (format == CellFormat.TEXT) {
        // 文字列形式(先頭ゼロやコード値をそのまま保持)
        cell.setCellValue(value.toString());
    } else if (format == CellFormat.INTEGER || format == CellFormat.DECIMAL) {
        if (value instanceof Number) {
            cell.setCellValue(((Number) value).doubleValue());
        } else {
            cell.setCellValue(value.toString());
        }
    } else if (format == CellFormat.DATE) {
        if (value instanceof java.util.Date) {
            cell.setCellValue((java.util.Date) value); // 表示はスタイルの日付書式に従う
        } else {
            cell.setCellValue(value.toString());
        }
    } else {
        cell.setCellValue(value.toString());
    }
}
コード例
package com.example.export;

import java.text.SimpleDateFormat;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

/**
 * テンプレート(xlsm)を読み込み、DB から取得したデータを転記して出力する。
 *
 * 処理の流れ
 *   1. controller から呼ばれる
 *   2. フィールドの DAO で DB から特定クラス型(RecordModel)のリストを取得
 *   3. ファイル出力に必要な項目だけの dto(RecordDto)へコピー
 *   4. クラスパス上のテンプレートを開き、ヘッダー名で列を突き合わせて値を転記
 *   5. 一時ファイルへ書き出して返す
 *
 * テンプレートの前提
 *   - 1 行目にヘッダーが記載済み
 *   - データ行(2 行目以降、最大 100 行分)は全セルが「文字列」の表示書式
 *   - 対象外の列は背景色グレーが設定済み
 *   - そのため、スタイル・表示書式・背景色はコードで一切設定しない
 *
 */
public class ExcelExportService extends AbstractExporter {

    /** テンプレートのクラスパス位置(resources 配下、SQL ファイルと同じ階層に配置する) */
    private static final String TEMPLATE_PATH = "com/example/export/template.xlsm";

    /** ヘッダーが存在する行(0 始まり) */
    private static final int HEADER_ROW_INDEX = 0;

    /** データ開始行(0 始まり。ヘッダーの次の行) */
    private static final int DATA_START_ROW_INDEX = 1;

    /** 最大出力行(データ行) */
    private static final int MAX_OUTPUT_ROWS = 100;

    /**
     * DAO
     */
    private RecordDao recordDao;

    // ------------------------------------------------------------------
    // controller から呼ばれる入口
    // ------------------------------------------------------------------

    /**
     * 検索条件で取得したデータをテンプレートに転記して出力する。
     *
     * @param criteria 画面から渡された検索条件
     * @param fileDto  ファイル出力に関する情報(出力ファイル名など)
     * @return 生成した Excel ファイル
     */
    public File exportFile(SearchCriteria criteria, FileDto fileDto) {
        // 1. DAO で DB から取得(特定クラス型のリスト)
        List<RecordModel> models = recordDao.findByCriteria(criteria);

        // 2. ファイル出力に必要な項目だけの dto へコピー
        List<RecordDto> dtoList = toDtoList(models);

        // 3. 件数チェック(テンプレートのデータ行は最大 100 行のため)
        if (dtoList.size() > MAX_OUTPUT_ROWS) {
            throw new IllegalStateException(
                "出力行が上限を超えています: " + dtoList.size() + " > " + MAX_OUTPUT_ROWS);
        }

        // 4. テンプレートに転記して出力
        return writeToTemplate(dtoList, fileDto);
    }

    // ------------------------------------------------------------------
    // model -> dto コピー
    // ------------------------------------------------------------------

    /**
     * DB から取得した model を、ファイル出力用の dto へ詰め替える。
     * dto には出力する項目だけ定義すればよい。
     */
    private List<RecordDto> toDtoList(List<RecordModel> models) {
        List<RecordDto> dtoList = new ArrayList<RecordDto>();
        for (RecordModel m : models) {
            RecordDto dto = new RecordDto();
            dto.setShohinCode(m.getShohinCode());
            dto.setShohinName(m.getShohinName());
            dto.setSuryo(m.getSuryo());
            dto.setTanka(m.getTanka());
            dto.setKingaku(m.getKingaku());
            dto.setTorihikiCode(m.getTorihikiCode());
            dto.setTorihikiName(m.getTorihikiName());
            dto.setTantosha(m.getTantosha());
            dto.setTorokubi(m.getTorokubi());
            dto.setKoshinbi(m.getKoshinbi());
            dto.setBiko(m.getBiko());
            dto.setKanriNo(m.getKanriNo());
            dtoList.add(dto);
        }
        return dtoList;
    }

    // ------------------------------------------------------------------
    // 列定義(dto フィールド名・ヘッダー名・値の種別)
    // ------------------------------------------------------------------

    /**
     * 値の種別。テンプレートが全セル文字列書式のため、見た目の制御には使わない。
     * 値を文字列へ変換する方法(日付の整形など)の判断にだけ使う。
     */
    private enum ValueType { TEXT, NUMBER, DATE }

    /** 1 列分の定義(dto フィールド名・ヘッダー名・値の種別) */
    private static class ColumnDef {
        final String fieldName; // dto のフィールド名
        final String header;    // ヘッダー名(テンプレートのヘッダーと突き合わせる)
        final ValueType type;   // 値の種別

        ColumnDef(String fieldName, String header, ValueType type) {
            this.fieldName = fieldName;
            this.header = header;
            this.type = type;
        }
    }

    /** 列定義一覧(列位置は持たない。位置はテンプレートのヘッダーから決まる) */
    private static final List<ColumnDef> COLUMNS = buildColumns();

    private static List<ColumnDef> buildColumns() {
        List<ColumnDef> list = new ArrayList<ColumnDef>();
        // フィールド名,            ヘッダー名,      値の種別
        list.add(new ColumnDef("shohinCode",   "商品コード",   ValueType.TEXT));
        list.add(new ColumnDef("shohinName",   "商品名",       ValueType.TEXT));
        list.add(new ColumnDef("suryo",        "数量",         ValueType.NUMBER));
        list.add(new ColumnDef("tanka",        "単価",         ValueType.NUMBER));
        list.add(new ColumnDef("kingaku",      "金額",         ValueType.NUMBER));
        list.add(new ColumnDef("torihikiCode", "取引先コード", ValueType.TEXT));
        list.add(new ColumnDef("torihikiName", "取引先名",     ValueType.TEXT));
        list.add(new ColumnDef("tantosha",     "担当者",       ValueType.TEXT));
        list.add(new ColumnDef("torokubi",     "登録日",       ValueType.DATE));
        list.add(new ColumnDef("koshinbi",     "更新日",       ValueType.DATE));
        list.add(new ColumnDef("biko",         "備考",         ValueType.TEXT));
        list.add(new ColumnDef("kanriNo",      "内部管理番号", ValueType.TEXT));
        return list;
    }

    /**
     * フィールド名に対応する dto の値を取り出す。
     * メソッド参照を使わず if 分岐で明示する。列を追加したらここにも 1 行足す。
     */
    private Object getValue(RecordDto dto, String fieldName) {
        if ("shohinCode".equals(fieldName))   return dto.getShohinCode();
        if ("shohinName".equals(fieldName))   return dto.getShohinName();
        if ("suryo".equals(fieldName))        return dto.getSuryo();
        if ("tanka".equals(fieldName))        return dto.getTanka();
        if ("kingaku".equals(fieldName))      return dto.getKingaku();
        if ("torihikiCode".equals(fieldName)) return dto.getTorihikiCode();
        if ("torihikiName".equals(fieldName)) return dto.getTorihikiName();
        if ("tantosha".equals(fieldName))     return dto.getTantosha();
        if ("torokubi".equals(fieldName))     return dto.getTorokubi();
        if ("koshinbi".equals(fieldName))     return dto.getKoshinbi();
        if ("biko".equals(fieldName))         return dto.getBiko();
        if ("kanriNo".equals(fieldName))      return dto.getKanriNo();
        throw new IllegalArgumentException("未定義のフィールド名: " + fieldName);
    }

    // ------------------------------------------------------------------
    // テンプレート取得・ヘッダー突き合わせ
    // ------------------------------------------------------------------

    /** クラスパス上のテンプレートを InputStream で取得する */
    private InputStream openTemplate() {
        InputStream is = getClass().getClassLoader().getResourceAsStream(TEMPLATE_PATH);
        if (is == null) {
            throw new IllegalStateException("テンプレートが見つかりません: " + TEMPLATE_PATH);
        }
        return is;
    }

    /** テンプレートのヘッダー行から「ヘッダー名 -> 列インデックス」を作る */
    private Map<String, Integer> buildHeaderIndex(Row headerRow) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        for (int j = 0; j < headerRow.getLastCellNum(); j++) {
            Cell cell = headerRow.getCell(j);
            if (cell != null && cell.getCellType() == Cell.CELL_TYPE_STRING) {
                String header = cell.getStringCellValue().trim(); // 前後空白を除去
                map.put(header, Integer.valueOf(j));
            }
        }
        return map;
    }

    /**
     * 列定義のヘッダーがすべてテンプレートに存在するか確認する。
     * テンプレートとコードのヘッダー定義がずれたときに、原因が分かる形で早期に落とす。
     */
    private void validateColumns(Map<String, Integer> headerIndex) {
        List<String> missing = new ArrayList<String>();
        for (ColumnDef col : COLUMNS) {
            if (!headerIndex.containsKey(col.header)) {
                missing.add(col.header);
            }
        }
        if (!missing.isEmpty()) {
            throw new IllegalStateException(
                "テンプレートに存在しないヘッダーがあります: " + missing);
        }
    }

// ------------------------------------------------------------------
    // テンプレートへの転記・出力
    // ------------------------------------------------------------------
    /**
     * テンプレートを開き、ヘッダー名で列を突き合わせて値を転記し、一時ファイルへ出力する。
     * スタイル・表示書式・背景色はテンプレート側に設定済みのため、コードでは触らない。
     */
    private File writeToTemplate(List<RecordDto> dtoList, FileDto fileDto) {
        InputStream is = openTemplate();
        XSSFWorkbook workbook = null;
        FileOutputStream fos = null;
        File outputFile;
        try {
            // テンプレート(xlsm)を開く。ヘッダー・書式・背景色、マクロはそのまま保持される
            workbook = new XSSFWorkbook(is);
            XSSFSheet sheet = workbook.getSheetAt(0);
            // テンプレートのヘッダーから対応表を作り、列定義との整合を検証する
            Row headerRow = sheet.getRow(HEADER_ROW_INDEX);
            Map<String, Integer> headerIndex = buildHeaderIndex(headerRow);
            validateColumns(headerIndex);
            // データ行へ転記(テンプレートに書式付き空行が用意されている前提)
            int rowIndex = DATA_START_ROW_INDEX;
            for (RecordDto dto : dtoList) {
                Row row = sheet.getRow(rowIndex);
                if (row == null) {
                    // 書式付き空行が足りない場合の保険(書式は付かない)
                    row = sheet.createRow(rowIndex);
                }
                rowIndex++;
                for (ColumnDef col : COLUMNS) {
                    int colIndex = headerIndex.get(col.header).intValue(); // ヘッダー名で列を引く
                    Cell cell = row.getCell(colIndex);
                    if (cell == null) {
                        cell = row.createCell(colIndex);
                    }
                    // 値だけ設定。setCellStyle は呼ばない(テンプレートの書式をそのまま使う)
                    setCellValue(cell, getValue(dto, col.fieldName), col.type);
                }
            }
            // 一時ファイルへ書き出す。テンプレートが xlsm のため出力も xlsm
            outputFile = File.createTempFile("record_export_", ".xlsm");
            fos = new FileOutputStream(outputFile);
            workbook.write(fos);
        } catch (IOException e) {
            throw new RuntimeException("テンプレート出力に失敗しました", e);
        } finally {
            try { if (fos != null) fos.close(); } catch (IOException ignore) {}
            // workbook.close() は v3.9 に存在しないため呼ばない(GC に任せる)
            try { if (is != null) is.close(); } catch (IOException ignore) {}
        }
        return outputFile;
    }

    /**
     * 値をセルに設定する。テンプレートが文字列書式のため、すべて文字列として入れる。
     * 日付は表示用に整形した文字列にする。
     */
    private void setCellValue(Cell cell, Object value, ValueType type) {
        if (value == null) {
            cell.setCellValue("");
            return;
        }
        if (type == ValueType.DATE && value instanceof java.util.Date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
            cell.setCellValue(sdf.format((java.util.Date) value));
        } else {
            cell.setCellValue(value.toString());
        }
    }
}

// マクロ
Sub StampCell()
    ThisWorkbook.Sheets(1).Range("Z1").Value = "macro ran " & Now
End Sub

次の段階(テンプレートファイルの読み込み)

テンプレートを使う場合、new XSSFWorkbook(new FileInputStream(templateFile)) でテンプレートを開き、ヘッダー行を再生成せず既存のシートにデータ行だけを追記する。マクロ込みの .xlsm テンプレートを土台にする場合は、開いて書き戻すだけで VBA は保持される。

注意点

  • 実行経路上のメソッドに private 処理を差し込み、独自 Response 型を返す。経路外のメソッドを return null で潰す場合は、戻り値が参照されないことを Call Hierarchy で確認する。参照されるなら空オブジェクトを返す

  • 最大100行のため XSSFWorkbook で十分 … メモリを抑えるための SXSSFWorkbook は不要。SXSSFWorkbook 固有の dispose() も登場しない。XSSFWorkbookclose() のみでよい

  • スタイルの使い回し … 行・セルごとに createCellStyle を呼ぶとスタイル数上限に達するリスクがある。対象用・対象外用を1回ずつ生成して使い回す

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?