Salesforce開発者がBulk API 2.0で踏んだ地雷と後悔

bulk api 2.0 で実際にやらかした判断

あれはまだ私がSalesforce開発者としてデータ移行案件に携わり始めて間もない頃、とある大規模な初期データ投入プロジェクトがあった。数百万件規模の顧客マスターと、それぞれ数百万から数千万件の子オブジェクト(連絡先、契約情報など)をSalesforceに移行する、という内容だ。

当時、Salesforceの公式ドキュメントでBulk API 2.0が推されていた。シンプル、高速、自動バッチ処理。いいことずくめに見えた。Bulk API 1.0は、バッチを自分で切って、それぞれのバッチのステータスを追う必要があって少し煩雑そうだな、と直感的に感じた。「新しいものの方が優れているだろう」という安易な思い込みと、「自動でいい感じにやってくれるなら、その分コーディングの手間が省ける!」という楽観的な判断で、私は何の疑いもなくBulk API 2.0の採用を決めた。

今振り返れば、この「自動でいい感じにやってくれる」という機能への過信こそが、最初のやらかしだった。

「親がいないのに子を登録するな」という至極当然のエラー

最初の数オブジェクトは順調だった。独立したオブジェクトは、CSVファイルをS3にアップロードし、Bulk API 2.0でジョブを作成、ファイルを指定して実行すれば、たしかに手間なく大量データが投入されていく。素晴らしい、とさえ思った。

しかし、親オブジェクトと子オブジェクトが絡むデータ投入で、地雷を踏んだ。

たとえば、Account(取引先)とその子オブジェクトであるContact(取引先責任者)を投入するケースだ。ContactにはAccountIdという参照項目があり、これがAccountのIDを参照する。

私は、親オブジェクトであるAccountと子オブジェクトであるContactのCSVファイルをそれぞれ用意し、まずはAccountのジョブを投入。その後に、AccountのIDをルックアップしてContactAccountIdを設定したCSVを作成し、Contactのジョブを投入した。ここまでは良い。

問題は、親と子の複数のオブジェクトを「まとめて」「自動で」投入できると勘違いしていた点だ。Bulk API 2.0は、あくまで単一のオブジェクトに対する操作をシンプルにするものであって、オブジェクト間の参照整合性を自動で担保してくれるわけではない。

「自動でやってくれる」という言葉が頭にこびりついていた私は、「複数のCSVをまとめて指定すれば、Salesforce側で賢く依存関係を解決して、いい感じに登録してくれるだろう」と、まるで夢を見ているかのような設計をしてしまったのだ。

結果は言わずもがな。Contactのジョブは大量のエラーを吐き出した。「参照先のAccountが見つかりません」といったエラーメッセージの嵐。そりゃそうだ。まだ登録されていない親レコードのIDを参照しているのだから。当時は、Bulk API 2.0の自動バッチ機能が「ファイル内のレコードの順序も考慮して、参照関係もいい感じに解決してくれる」と思い込んでいた節がある。実際には、それは完全に私の勘違いだった。

今ならこう言う。「Bulk API 2.0は、あくまで『単一オブジェクトの大量データ処理を最適化する』ためのAPIだ。参照関係のある複数オブジェクトを絡めて投入する場合は、依存関係の順序で個別にジョブを切り、各ジョブの完了を待ってから次のジョブを実行する必要がある」と。あるいは、外部IDを使って参照関係を解決するようにCSVを加工するか、だ。

エラーハンドリングは自動じゃない、手動だ!

もう一つの大きなやらかしは、エラーハンドリングの設計不足だった。

数百万件のデータ投入ともなれば、何かしらの理由で一部のレコードが失敗するのは避けられない。データクリーニングの不足、ガバナ制限、一時的な競合など、原因は多岐にわたる。

Bulk API 2.0は、ジョブ完了後にエラーレコードを含むCSVファイルをダウンロードできる機能がある。これもまた「自動でエラーファイルを出力してくれるから、リカバリも楽だろう」という私の判断ミスを招いた。

実際にエラーが発生し、エラーファイルをダウンロードしてみると、想像以上に複雑だった。エラーCSVには、元のレコードデータに加え、Salesforceが返したエラーメッセージと原因が記載されている。しかし、数百万件のうち数万件がエラーになった場合、その数万件をどう処理するのか?

  • エラー内容によって修正方法が異なる
  • 特定のエラーは無視して進めて良いのか?
  • 修正したエラーレコードを再投入する際、どうやって元のジョブと紐付け、重複投入を防ぐのか?
  • 失敗したレコードだけを抽出し、再加工して別のBulk API 2.0ジョブに投入する仕組みが必要だったが、そこまで考慮していなかった

結局、エラーCSVを手動で加工し、ExcelのVLOOKUPを駆使して修正し、再度アップロードするという、非常に時間と手間のかかる作業を強いられることになった。自動でやってくれるのは「エラーファイルを生成する」までで、その後のリカバリプロセスは、きっちり人間が設計し、実装する必要があったのだ。

当時は「エラーが発生したら、全部やり直せばいいか」くらいの感覚でいたが、大規模データ移行でそれは現実的ではない。投入に数時間かかるジョブを、エラーが出るたびに最初からやり直すなど狂気の沙汰だ。

今なら、エラーハンドリングとリトライ戦略はBulk API 2.0の利用とセットで考えるべき必須要件だと断言する。エラーファイルをダウンロードし、その内容を解析して、失敗したレコードだけを抽出・修正・再投入する専用のプロセスを必ず構築する。このプロセスがなければ、大規模なデータ移行は地獄絵図と化す。

// 当時の私の脳内設計図 (これだけでは全く足りなかった)
// create job
// upload csv
// close job
// poll job status until "JobComplete" or "Failed"
// if Failed, download error result file

// 今ならこう考えるべきだった (最低限の擬似コード)
// 1. JobCreateRequest (objectType, operation, externalIdFieldName)
// 2. UploadJobData (stream csv data)
// 3. CloseJob
// 4. Poll Job Status
//    if JobFailed:
//        DownloadErrorResults -> Parse errors -> Identify affected records -> Log errors
//        // ★ここからが重要★
//        // エラー内容に応じたリカバリロジック
//        // 例: INVALID_FIELD_FOR_INSERT_UPDATE -> データクレンジングチームへアラート
//        // 例: DUPLICATE_VALUE -> 外部IDを元に既存レコードとの紐付けを試みる
//        // 例: LOCK_ROW_UNAVAILABLE -> 一時的な競合なので、affected recordsだけを抽出してリトライキューに入れる
//        //
//        // リトライキューのレコードを加工し、新しいCSVを作成
//        // 5. 新しいJobCreateRequestでリトライジョブを作成し、上記1-4を繰り返す
//    else if JobComplete:
//        DownloadSuccessfulResults (Optional) -> Verify counts
//        Log successful job completion

特にLOCK_ROW_UNAVAILABLEのような、一時的なロック競合によるエラーは厄介だった。これは何度かリトライすれば成功する可能性が高いが、Bulk API 2.0は個別のレコードに対するリトライ機構を直接提供しているわけではない。結局、失敗したレコードを抽出し、別のジョブとして再実行するしかなかった。この「抽出して再実行」のロジックが、当時全く設計されていなかったのだ。

今なら別の選択をするか?

もしまた同様の大規模データ移行案件があれば、Bulk API 2.0を使うか? 答えは「使い方次第」だ。

単純なオブジェクトの大量投入、特に更新(UPSERT)で外部IDがしっかり整備されているようなケースでは、そのシンプルさは強力な武器になる。

しかし、私があの時やったような「複雑な参照関係を持つ複数のオブジェクトを、すべてBulk API 2.0の自動処理任せにする」という選択は、二度としない。

むしろ、よりきめ細やかな制御が必要な場合は、Bulk API 1.0のバッチ単位での制御や、Apex Batchによるサーバーサイドでの処理を検討するだろう。特に複雑なロジックを伴う更新や、外部IDが整備されていない初期データ投入で、既存レコードのルックアップが必要な場合は、Apex Batchの方が柔軟性が高い場合もある。

Bulk API 2.0は非常に便利だが、それは「使い方を理解し、その限界と特性を把握した上で」という前提がつく。当時の私は、その前提を深く考えず、ただ「新しいから」という理由で採用してしまった。この経験は、API選定の際にドキュメントを読み込むだけでなく、実際に発生しうるエラーケースとその対処法まで含めて検討する重要性を、痛感させてくれたものだ。

これは当時の自分向けのメモだ。


コメント