大規模なインポートは、接続の切断、無効なデータ、プロセスのタイムアウトなどによって失敗することがあります。すべての変更を単一のトランザクションに依存させると、ロックを長時間保持することになり、最後に一度だけ確定するまでの作業をすべて失うおそれがあります。一方、復旧計画なしに各行を個別に保存すると、中断によってデータに一貫性がなくなる可能性があります。
PHPインポートにおけるバッチトランザクションの設計とは、安全にコミット、記録、再実行できる作業単位を選ぶことです。インポート全体をアトミックにすることとは異なります。通常はバッチごとにアトミックであり、処理全体としては一部だけ適用された状態になることがあります。この違いは、実装だけでなく運用時の利用者体験にも反映させる必要があります。
単一のトランザクションが適さない理由

長時間のトランザクションはロックを保持し、トランザクションログへの負荷を増やし、アプリケーションのクエリと競合する可能性があります。数百万行を処理するインポートでは、残りの量や失敗後に何をすべきかを把握することも困難です。具体的な挙動はデータベースエンジン、分離レベル、書き込み量によって異なるため、実際の環境で測定するのが適切です。
行ごとにコミットすれば各トランザクションの時間は短くなりますが、コミットのコストが増え、行間の依存関係が中途半端に適用される可能性があります。バッチはその中間点です。グループ内の処理が失敗し、例外が伝播した場合、そのグループはロールバックされます。それ以前にコミット済みのバッチはそのまま残ります。アプリケーションはこれを全体の成功や全面的なロールバックではなく、部分的な進捗として伝える必要があります。
適切なトランザクション境界を選ぶ
作業単位は、常に固定件数の行である必要はありません。一貫性を保って検証・永続化できる変更を表す単位にします。各レコードが独立しているなら、行のまとまりで十分な場合があります。複数のレコードが一つのビジネスエンティティを構成する場合(たとえばヘッダーと明細)、それらを同じトランザクションに含めることで、不完全なエンティティの保存を防げます。
バッチサイズを決める際は、各バッチの処理時間、メモリ使用量、共有テーブルでの競合、エラーの頻度、作業をやり直すコストを評価します。大きなバッチはコミット回数を減らせますが、処理に時間がかかり、失敗時にやり直す作業も増えます。小さなバッチならそのコストを抑えられますが、オーバーヘッドが増え、性能が低下することがあります。万能なサイズはありません。設定可能な値から始め、実態に即した負荷テストで調整してください。
PHPでは、段階的に読み込める場合、ファイル全体をメモリに蓄積しないでください。上限を設けた行数を読み込み、フィールドを検証し、トランザクションを開始して書き込みを行い、コミットします。エラーが起きた場合はバッチのトランザクションをロールバックし、エラーを分類します。たとえばPDOでは、beginTransaction()、commit()、rollBack()で制御できます。例外処理では、失敗したトランザクションをコミットしようとしないようにする必要があります。
確実に再開できるよう進捗を記録する
チェックポイントは、ソースのどこまで処理が確定したかを示します。順序が変わっても依存せずに続きから処理できるなら、安定した識別子、ファイル内の位置、複合キーなどを使えます。ページ分割したクエリでは、処理中にレコードが追加・削除されると変化し得るオフセットより、安定したキーで進めるほうが安全な場合が多くあります。
進捗とバッチの変更は、整合するように更新する必要があります。可能であれば、業務データの書き込みと新しいチェックポイントを同じトランザクション内に保存します。そうすれば、コミット前にプロセスが停止しても、データベースに適用されていない進捗が記録されません。コミットされれば、カーソルも進みます。実行状態が別のシステムに保存される場合、このアトミック性は自動的には得られません。照合の仕組みを設計するか、バッチが再実行されることを前提に冪等性を確保する必要があります。
インポートID、状態、チェックポイント、各種カウンターに加え、必要に応じてファイルのバージョンやフィンガープリントへの参照も保存します。再開時には、同じ入力を対象としていることと、実行を復旧できる状態であることを確認してください。「保留中」「実行中」「完了」「失敗」などの明示的な状態と、放棄された実行を検出するポリシーを定義します。例外を出さずにプロセスが終了したというだけでインポートを完了扱いにせず、想定された終端に到達したことを確認してください。
エラーを切り分け、再試行を制御する
無効なレコードによって全体を停止するのか、除外するのか、レビュー対象として記録するのかを事前に決めます。予測できる形式エラーについては、トランザクションを開始する前に検証し、行の識別子とともに理由を記録すると役立つ場合があります。永続化エラーが発生した場合は、該当バッチをロールバックします。後続のバッチを続行するのが妥当なのは、それらのデータが独立しており、インポートが部分完了であることを明確に示せる場合に限られます。
必須値の欠落のような恒久的なエラーと、復旧可能な接続切断のような一時的な障害を区別します。恒久的なエラーを再試行するとリソースを浪費し、診断も難しくなります。一方、一時的な障害には、上限付きの試行回数と段階的に延ばす待機時間を設定できます。再試行を使い切った場合は、元の原因を隠すことなく調査・再開できるよう、十分な状態とコンテキストを保持してください。
慎重に設計しないと、再試行によって書き込みが重複することがあります。業務キーやソースIDを使って、インポート済みレコードを識別します。また、再実行時に挿入、更新、無視、競合の通知のいずれを行うかを決めます。データベースの一意制約はその判断を補強できますが、業務ロジックの代わりにはなりません。重複防止をチェックポイントだけに頼らないでください。チェックポイントが実態とずれたり、別経路で行われた変更をカバーできなかったりする可能性があります。
バッチでメール送信、メッセージの発行、API呼び出しも行う場合、データベースのトランザクションで外部への影響を取り消すことはできません。存在しない分散トランザクションの一部であるかのように実行しないでください。代替策として、トランザクショナルなアウトボックステーブルに未処理イベントを記録し、後から冪等性を確保する識別子と再試行を使って処理できます。外部処理に失敗した場合も、操作を重複させず、バッチが完全に終了したと誤って示すことなく復旧できる状態にする必要があります。
整合性、進捗、復旧をテストする
テストでは正常系だけでなく、さまざまな状況を網羅します。トランザクション開始前、書き込み中、コミット直前、コミット後かつ外部状態の更新前に障害を注入します。どのデータが残るか、どのチェックポイントが保持されるか、再開後に期待どおりの結果になるかを確認してください。重複レコード、無効な行、空のバッチ、永続化エラーも含めます。
実態に即したテストでは、バッチごとの処理時間、メモリ使用量、エラー率、ロック時間、進捗を測定します。また、カウンターが読み込み済み、受け入れ済み、拒否済み、コミット済みの行を区別することも確認してください。「処理済み」は、ロールバックされた変更を含む場合に曖昧になり得ます。メトリクスとログではインポートとバッチを識別できるようにしつつ、診断に不要な機密データを出力しないでください。
本番環境投入前のチェックリスト

- トランザクション単位が、一貫性のある業務上の単位と一致している。
- バッチサイズを設定でき、代表的なデータと負荷で評価済みである。
- 該当するバッチがコミットされた場合にのみチェックポイントが進む。
- 再開戦略が安定したキーを使い、ソース入力を識別できる。
- 恒久的なエラーと一時的な障害に、異なる観測可能な処理を適用している。
- 操作の再実行が冪等であるか、競合を明示的に検出する。
- 外部への影響を、独立した復旧可能な戦略で処理している。
- 処理全体の成功だけでなく、さまざまな箇所での障害をテストしている。
目標はあらゆる障害を防ぐことではなく、その影響を抑え、結果の状態を明確にすることです。適切なトランザクション境界、一貫性のあるチェックポイント、安全な再試行により、PHPインポートは部分的な進捗を完了と取り違えたり、確定済みの変更を再適用したりせずに復旧できます。



