LoginSignup
0
0

Easy ExcelでWrite操作を行う際のセルの結合方法は以下の通りです。

Posted at

@ExcelMerge(自分で追加した):

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelMerge {


}

MergeHandler:

public class CustomAbstractMergeStrategy<T, R> implements WorkbookWriteHandler {

    /**
     * 結合する行数
     */
    private List<Integer> mergeRowCountList;

    /**
     * 結合が必要な列のインデックス
     */
    private List<Integer> mergeColumnIndexList;

    /**
     * オブジェクト
     */
    private Class<T> clazz;

    /**
     * セルの結合ルール:
     * 1. データにのみ適用、ヘッダーには適用しない
     * 2. 結合が必要なフィールドは @ExcelMerge で注釈が必要
     * 3. 結合条件は: 入力パラメータ Function<T, R> function が必要で、List<T> list 内の結合するセルが連続している必要あり(ソートが必要な場合もある)
     *
     * @param clazz
     * @param list
     * @param function
     */ 
    private CustomAbstractMergeStrategy(Class<T> clazz, List<T> list, Function<T, R> function) {
        if (CollUtil.isNotEmpty(list)) {
            Map<R, Long> collect = list.stream().collect(Collectors.groupingBy(function, LinkedHashMap::new, Collectors.counting()));
            this.mergeRowCountList = collect.values().stream().map(Long::intValue).collect(Collectors.toList());
            this.clazz = clazz;
            this.mergeColumnIndexList = new ArrayList<>();
            markMergeIndex();
        }
    }

    public static <T, R> CustomAbstractMergeStrategy<T, R> of(Class<T> clazz, List<T> list, Function<T, R> function) {
        return new CustomAbstractMergeStrategy<>(clazz, list, function);
    }

    /**
     * 結合する列のインデックスをマーク
     */
    private void markMergeIndex() {
        Field[] fields = clazz.getDeclaredFields();
        int columnIndex = 0;
        for (Field field : fields) {
            if (field.isAnnotationPresent(ExcelProperty.class)) {
                if (field.isAnnotationPresent(ExcelMerge.class)) {
                    mergeColumnIndexList.add(columnIndex);
                }
                columnIndex++;
            }
        }
    }

    /**
     * ワークブック作成前に呼び出される
     */
    @Override
    public void beforeWorkbookCreate() {

    }

    /**
     * ワークブック作成後に呼び出される
     *
     * @param writeWorkbookHolder
     */
     @Override
     public void afterWorkbookCreate(WriteWorkbookHolder writeWorkbookHolder) {

     }

    /**
     * ワークブックの全操作後に呼び出される
     *
     * @param writeWorkbookHolder
     */
     @Override
     public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) {
         if (CollUtil.isEmpty(mergeColumnIndexList)) {
             return;
         }
         if (CollUtil.isEmpty(mergeRowCountList)) {
             return;
         }
         Sheet sheet = writeWorkbookHolder.getWorkbook().getSheetAt(0);
         // 0行目がヘッダー、1行目からデータ
         int rowIndex = 1;
         for (Integer mergeRowCount : mergeRowCountList) {
             if (mergeRowCount > 1) {
                 for (Integer columnIndex : mergeColumnIndexList) {
                     CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndex, rowIndex + mergeRowCount - 1, columnIndex, columnIndex);
                     sheet.addMergedRegionUnsafe(cellRangeAddress);
                 }
                 rowIndex += mergeRowCount;
             } else {
                 rowIndex += 1;
             }
         }
     }


}

Data:

@Data
public class Data implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 計画ID
     */ 
    @ExcelProperty(value = "計画ID")
    @ExcelMerge
    private Integer id;

    /**
     * 試験計画
     */
    @ExcelProperty(value = "試験計画") 
    @ExcelMerge
    private String examPlanName;

    /**
     * 種類
     */
    @ExcelProperty(value = "種類")
    private String subjectTypeName;

    /**
     * 科目名
     */
    @ExcelProperty(value = "科目名")
    private String examSubjectName;

    /**
     * 問題数
     */
    @ExcelProperty(value = "問題数")
    private Integer questionCount;

    /**
     * 受験人数
     */
    @ExcelProperty(value = "受験人数")
    private Integer examCount;

}

Util:

 public class Util{

        public static <T> void exportExcelWithMerge(List<T> data, Class<? extends T> cls, String fileName, Integer sheetNo, String sheetName, WriteHandler writeHandler) {
            HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
            try (OutputStream out = response.getOutputStream()) {
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.setCharacterEncoding("utf-8");
                fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
                response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
                EasyExcel.write(out, cls).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).registerWriteHandler(writeHandler).sheet(sheetNo, sheetName).doWrite(data);
            } catch (IOException e) {
                log.error("error", e);
                throw new BusinessException(BizErrorCodeEnum.FILE_DOWNLOAD_FAILED);
            }
        }

        public static <T> void exportExcelWithMerge(List<T> data, Class<? extends T> cls, String fileName, WriteHandler writeHandler) {
            exportExcelWithMerge(data, cls, fileName, 0, "Sheet1", writeHandler);
        }

    }

Test:

    @PostMapping("/export")
    @ApiOperation("export")
    public void export(@RequestBody @Validated ExamPlanExportReq req) {
         List<ExportExamPlanResp> data = getDataFromDb(req);
        FileUtil.exportExcelWithMerge(data, ExportExamPlanResp.class, "list", CustomAbstractMergeStrategy.of(ExportExamPlanResp.class, data, ExportExamPlanResp::getId));
    }

WorkbookWriteHandlerを使用する理由は、すべてのデータの解析が完了した後にafterWorkbookDisposeメソッドが呼び出されるためです。

セルの結合は、RowWriteHandlerやCellWriteHandlerでも実装可能ですが、CellWriteHandlerで行うのは非効率です。

CellWriteHandlerでは各行の各セルを反復処理する必要があるため、
M行 x N列のデータがある場合、結合処理にM x N回の反復が必要になります。

RowWriteHandlerの方が良いのですが、それでもM回の反復処理が必要です。

そのため、WorkbookWriteHandlerを使うのが最も効率的です。
すべてのデータの解析が完了した後、一度だけ結合処理を行えば良いので、とても高速に動作します。

以上の理由から、セルの結合にはWorkbookWriteHandlerを使うのがベストであると判断し、
この実装を選択しました。効率的な処理を行うための設計です。

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