Visualforce で CSV ダウンロード＆ DML 実行時に「DML currently not allowed」を回避する方法

## はじめに
Salesforce の Visualforce ページで CSV ダウンロード機能を実装した際、「DML currently not allowed」 というエラーに遭遇することがあります。

特に
- ページのコンストラクタでデータの更新（Database.update() など）を呼び出している
- readOnly="true" なページで DML を実行している
- getContent() などでVFページを呼び出している
といったケースで、このエラーが出ることがよくあります。

本記事では、実際に遭遇した 「DML currently not allowed」エラー をどのように回避して要件を満たしたか、そのアプローチをまとめます。

## 今回の課題
#### 「VisualforceページでCSVをダウンロードする前後にレコードを更新したい」
具体的には、CSV をダウンロードしたタイミングで 出力したレコードに出力済(仮にDownloadedFlag__cと定義する) などを true に更新する要件を実装しました。
初期実装では、`Visualforce コントローラクラスの コンストラクタ内 で SOQL 取得と DML をまとめて書いてしまっていた`

その結果、「DML currently not allowed」 例外が発生してページがエラーになる状況。

## なぜエラーになるのか？
Salesforce では、Visualforceページの初期読み込み（デフォルトGETリクエスト） が 読み取り専用モード（Read-Only モード） になる場合があります。

代表的な例:
- `<apex:page>` タグで readOnly="true" を指定している
- PageReference.getContent() や renderAs などでPDF/CSV出力をすると自動的に読み取り専用になる
この「読み取り専用モード」下で DML（Insert、Update、Delete など）を行うと、
「DML currently not allowed」 というエラーが発生する仕組みになっています。

## 解決策
#### ポイントは「コンストラクタやページ初期読み込み時に DMLしない」こと です。
つまり、CSV ダウンロード用のロジックと DML 更新ロジックを別のタイミング・メソッドに分ける と、エラーを回避できます。

今回のアプローチ
1. コンストラクタで「表示用 or CSV出力用データ」の取得のみ行う
コンストラクタでは DML は行わず、「どのレコードをダウンロード対象にするか？」などの判定や、必要なレコードの SOQL 取得だけを行う。

2. action メソッド で DML を実行
Visualforce には `<apex:page action="{!methodName}" />` という仕組みがあります。
ページ読み込み時に自動的に呼ばれるメソッドですが、コンストラクタ実行後のアクション なので、ここでなら DML が許される状況を作れる場合があります。
※注意: もし readOnly="true" が明示的に指定されていると、やはり DML は禁止されるので、その場合は readOnly="false" にするか、別のアプローチを検討してください。

- コード例（ざっくりイメージ）
```html
<!-- CSVダウンロードページ -->
<apex:page 
    controller="DownloadCSVController"
    readOnly="false"  <!-- 必要に応じてfalseにする -->
    action="{!doUpdateRecords}"
    contentType="text/csv;charset=Shift_JIS;#somefilename.csv"
    language="ja"
>
    "ヘッダー１","ヘッダー２","ヘッダー３"
    <apex:repeat value="{!csvDataList}" var="row">
        "{!row.col1}","{!row.col2}","{!row.col3}"
    </apex:repeat>
</apex:page>
```

```java
public with sharing class DownloadCSVController {

    public List<YourObject__c> csvDataList { get; set; }

    // コンストラクタ：データの取得だけ
    public DownloadCSVController() {
        // 例: CSV出力に必要なデータだけ Query
        csvDataList = [
            SELECT Id, Name, ... FROM YourObject__c
            WHERE ...
            // ※この時点ではDMLしない
        ];
    }

    // actionメソッド：更新などのDMLを行う
    public PageReference doUpdateRecords() {
        // CSV ダウンロード直前にフラグを立てたい、など
        for (YourObject__c rec : csvDataList) {
            rec.DownloadedFlag__c = true;
        }
        update csvDataList;  // ← DMLを実行

        // ページ遷移（本ページ自身を再読み込み）しない場合は null を返す
        return null;
    }

    // Visualforce 側の <apex:repeat> で CSV用データを表示
}

```

上記のように constructor(データ取得) → action(更新) → CSV描画 の順序で実行されれば、DML currently not allowed は発生しません。

## 別の方法・応用
1. ダウンロード前か後に別ページ or 別ApexメソッドでDML する
たとえば LWC や Aura コンポーネントで事前に DownloadedFlag__c を更新してから CSV ページにリダイレクトする方法。
またはダウンロードが終わってからボタン押下などで DML する方法。
2. Visualforce をやめて LWC + Apex のみで CSV 作成・ダウンロード
Apex 側で Blob の CSV データを生成し、LWC で Base64 受け取り → download する手段もあります。
ただし、非常に大量のデータの場合は Visualforce の readOnly="true" による SOQL 行数拡張 (最大1,000,000行まで) が便利なため、要件次第です。


## まとめ
- 「DML currently not allowed」エラーが起きる理由
  - Visualforce ページが 読み取り専用モード で実行されており、コンストラクタや初期描画フェーズでは DML が禁止されているから。
- 回避ポイント
  - 「CSV生成とDMLを同じリクエスト（コンストラクタ）でやらない」
    - 別メソッド (action など) に分けて DML する ことで解決できる。
        - メリット
            - これで コンストラクタ でのDMLを排除しつつ CSVデータの取得 & Update の両方が可能になる。
            - readOnly="false" にしたり、事前に別アクションでDMLを済ませておけば、ページ描画時にデータも更新され、要件を満たせる。

同じような課題（ダウンロード時にフラグを立てたい等）に直面した方の参考になれば幸いです。