まだSalesforce開発に本格的に携わり始めたばかりの頃、外部システムとの連携でSOAP APIの`update`コールを使って大量データを更新するバッチ処理を組むことになりました。数千件のレコードを外部システムからSalesforceに毎日連携する、というシンプルな要件です。
私は当時、JavaのAxis2を使ったクライアントを構築していました。数百件単位でデータをチャンクに分け、1リクエストあたり最大200件(SalesforceのGovernor Limitを意識して)のSObjectリストをSOAP APIの`update()`メソッドに渡していました。ここまでは良かったんです。
当時の私の判断はこうでした。SOAP APIの`update()`メソッドは`SaveResult[]`を返します。この`SaveResult`配列をループし、それぞれの`saveResult.isSuccess()`が`true`なら成功、`false`なら失敗。という認識でした。非常にシンプルで、初歩的なエラーハンドリングだと思っていました。
// 当時の不十分なエラーハンドリング(イメージ)
// List<SObject> recordsToUpdate; // 200件以下のリスト
// SforceService binding; // 初期化済み
SaveResult[] results = binding.update(recordsToUpdate.toArray(new SObject[0]));
for (int i = 0; i < results.length; i++) {
SaveResult sr = results[i];
if (!sr.isSuccess()) {
// ここでログ出力したり、連携元にエラーを通知したり...
// しかし、実際にはこのチャンク全体の更新が失敗したと認識していた
System.err.println("レコード更新に失敗しました。詳細不明:" + sr.getId()); // IDは出力してたけど、それがどのレコードかは意識が薄かった
// ... このチャンクの全レコードを失敗扱い ...
} else {
// ... 成功扱い ...
}
}
これが、後々大きな問題を引き起こしました。ある日、連携元システムから「データが正しく更新されていない」という問い合わせが来ました。私のバッチ処理のログには、`isSuccess()`が`false`になったというエラーが記録されており、私はそのチャンク内の200件全てが失敗したと認識していました。なので、連携元には「この時間のデータは全件失敗しました」と伝えていました。
しかし、Salesforce側を確認すると、そのチャンク内の200件のうち、例えば10件だけがエラーで、残りの190件は正しく更新されていたのです。当時の私は「部分成功」という概念をSOAP APIの`update`の戻り値で真剣に考えていませんでした。
`SaveResult`オブジェクトには、`isSuccess()`以外に`getErrors()`というメソッドがあります。これが肝心でした。`isSuccess()`が`false`の場合でも、それはあくまでその個別のSObjectに対するDML操作が失敗したことを示しているだけで、同じリクエスト内の他のSObjectについては成功している可能性があるのです。つまり、1回の`update`コールで渡したSObjectリストのうち、一部だけがエラーになり、残りは成功する「部分成功」という状態が頻繁に発生します。
私の当時のコードでは、`isSuccess()`が`false`になった時点で「このチャンク全部ダメだ」と判断し、エラーログだけを出して次のチャンクに進んでいました。連携元システムも、私が「全件失敗」と伝えたため、当該時間のデータを再度送り直したり、手動でリカバリしたりする手間が発生しました。実際には190件は成功しているので、二重更新のリスクもはらんでいました。
この時は、何が成功して何が失敗したのかを正確に把握し、連携元システムに伝えることの重要性を痛感しました。
今ならこうする:`getErrors()`の活用
もし今、同じような状況に直面したら、`SaveResult`の処理はもっと細かく行います。特に`getErrors()`の中身をしっかりと見て、どのレコードがどんな理由で失敗したのかを明確にします。
// 今ならこう書く(イメージ)
// List<SObject> recordsToUpdate; // 200件以下のリスト
// SforceService binding; // 初期化済み
Map<String, SObject> idToOriginalRecordMap = new HashMap<>();
for(SObject record : recordsToUpdate) {
idToOriginalRecordMap.put(record.getId(), record); // 元のレコードとIDを紐付けておく(新規作成ならIDはnullなので注意)
}
SaveResult[] results = binding.update(recordsToUpdate.toArray(new SObject[0]));
List<String> successfulIds = new ArrayList<>();
List<Map<String, String>> failedRecordsDetails = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
SaveResult sr = results[i];
SObject originalRecord = recordsToUpdate.get(i); // i番目のレコードが対応
if (sr.isSuccess()) {
successfulIds.add(sr.getId());
} else {
StringBuilder errorMessages = new StringBuilder();
for (com.sforce.soap.partner.Error error : sr.getErrors()) {
errorMessages.append(" - ").append(error.getStatusCode()).append(": ").append(error.getMessage());
}
Map<String, String> errorDetail = new HashMap<>();
errorDetail.put("OriginalRecordId", (originalRecord.getId() != null) ? originalRecord.getId() : "N/A (New Record)");
errorDetail.put("SalesforceResultId", sr.getId()); // 成功した場合はIDが入るが、失敗時でも一部入ることがある
errorDetail.put("ErrorMessages", errorMessages.toString());
failedRecordsDetails.add(errorDetail);
}
}
// 処理後:成功したIDリストと、失敗したレコードの詳細リストを連携元に通知する
// ... logging and notification logic ...
この修正によって、どのレコードが成功し、どのレコードがどんな理由で失敗したのかが明確になります。連携元システムも、成功したレコードは再送不要、失敗したレコードのみ再送やデータ修正を行う、といった適切なリカバリ処理を組むことができるようになります。
当時この問題に気づくまで、デバッグと連携元との調整に丸1日以上を費やしました。今では笑い話ですが、あの時の「なぜだ!?」という焦りは今でも覚えています。
SOAP APIに限らず、SalesforceのDML操作の戻り値は、常に部分成功を意識して詳細まで確認するべきだと、この経験から学びました。
これは当時の自分向けのメモだ。
コメント
コメントを投稿