SXSSFWorkbookを利用する際の個人的な注意点 (行単位のアクセス単位は避けたほうが無難 / 既存のxlsxファイルで書き込み済みの行にはアクセスできない)

# 概要

Apache POIでXSSFWorkbookを利用する場合、頭をもたげてくるのはメモリ問題。XSSFWorkbookは読み込んだデータや書き込んだデータをすべてメモリ上に展開します。そのため、大きなサイズのExcelを作ったり読んだりするときはよくよく注意しないと、OutOfMemoryErrorを起こしがちです。

Apache POIではこのXSSFWorkbookのメモリ食いすぎ問題に対応するため、SXSSFWorkbookという、全データをメモリには展開せず、一時ファイルに書き出すことで、メモリ消費量を節約するAPIが用意されています。XSSFWorkbookと同じWorkbookインターフェースを実装しているので、

```java
Workbook book = new XSSFWorkbook();
// ↓
Workbook book = new SXSSFWorkbook();
```

というように、実装クラスだけ差し替えて、メモリ消費量節約を実現した気になることが多いのですが、SXXFWorkbookには利用上の注意点があります。

# 1. 行単位のアクセス単位は避けたほうが無難

SXSSFWorkbookのメモリ節約ロジック、つまり一時ファイルへの書き出しロジックは「`windowSize`行だけをメモリ上に保持し、それを超える行を作ろうとした瞬間に、それより前の行はすべて一時ファイルに書き出す」というものです。そして、一時ファイルに書き出された前の行については、アクセスすることはできません。

これは以下のようなソースコードで確認することができます。


```java
try (Workbook book = new SXSSFWorkbook()) {
    Sheet sheet = book.createSheet();

    // 2行目から1000行目まで書き込み。
    for (int i = 1; i < 1000; i++) {
        sheet.createRow(i).createCell(0).setCellValue(String.valueOf(i));
    }

    // 2行目に書き込み忘れがあったので、2行目のRowを取ろうとしても
    // 戻り値がnullになるため、書き込めない
    sheet.getRow(1); // => null

    // 1行目に書き込み忘れがあったので、1行目のRowを作ろうとしても
    // 例外(*)が発生して、書き込めないどころか、アプリケーションが終了してしまう。
    sheet.createRow(0);
} catch (IOException e) {
    e.printStackTrace();
}
```

上記の(*)で発生する例外は次の通りです。

```
Exception in thread "main" java.lang.IllegalArgumentException: Attempting to write a row[0] in the range [0,899] that is already written to disk.
	at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:131)
	at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:65)
	at poi.Main.main(Main.java:25)

```

一応、メモリ上に保持されている行については、ランダムアクセスは可能です。メモリに保持する行を決める`windowSize`についても、コンストラクタやセッターで変更可能ですが、制御がややこしくなりがちで、それに伴うバグも生み出しかねないと考えています。個人的にはSXSSFWorkbookを利用する場合は、行単位のランダムアクセスは避けて、上の行から下の行にかけて順次アクセスするほうがよいと思います。

# 2. 既存のxlsxファイルで書き込み済みの行にはアクセスできない

既存のxlsxファイルにSXSSFWorkbookを使って、データを書き込みたいということもあると思います。このとき注意したいのは__「既存のxlsxファイルで書き込み済みの行は一時ファイルに書き込まれてしまい、SXSSFWorkbookではアクセスができない」ということです。__

たとえば、2行目から1000行目まで書き込み済みのエクセルファイル`2-1000.xlsx`があったとします。これを読み込んで、書き込み済みの行にアクセスしてみます。

```java
try (Workbook book = new SXSSFWorkbook(new XSSFWorkbook("2-1000.xlsx"))) {
    Sheet sheet = book.getSheetAt(0);

    // 2行目に書き込み忘れがあったので、2行目のRowを取ろうとしても
    // 戻り値がnullになるため、書き込めない
    sheet.getRow(1); // => null

    // 1行目に書き込み忘れがあったので、1行目のRowを作ろうとしても
    // 例外(*)が発生して、書き込めないどころか、アプリケーションが終了してしまう。
    sheet.createRow(0);
} catch (IOException e) {
    e.printStackTrace();
}
```

上記の(*)で発生する例外は次の通りです。

```
Exception in thread "main" java.lang.IllegalArgumentException: Attempting to write a row[0] in the range [0,999] that is already written to disk.
	at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:138)
	at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:65)
	at poi.Main.main(Main.java:21)
```

「テンプレートファイルみたいなものをシステム内に持っていて、バッチ処理やオンライン処理において、テンプレートファイルにデータを書き込み、結果ファイルをユーザが利用する」というユースケースがありがちですが、こういうユースケースではきちんと設計しないと、SXSSFWorkbookが利用できないということです。

# 環境情報 (pom.xml抜粋)

```xml
<dependencies>
  <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
  </dependency>

  <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
  </dependency>
</dependencies>

<properties>
  <maven.compiler.source>11</maven.compiler.source>
  <maven.compiler.target>11</maven.compiler.target>
</properties>
```

# 参考

- https://poi.apache.org/components/spreadsheet/how-to.html#sxssf
- https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/streaming/SXSSFWorkbook.html