【Apex】一括insertや一括updateでエラーレコードを特定する。データローダと同じ挙動を実現。
Apex開発でクセがあるのがガバナ制限。そのガバナを回避するために、insertやupdateなどのDMLは一括で発行するように書くのが定石です。
1 2 3 4 5 |
List<Account> accList = [SELECT Id, Name, Description FROM Account]; for (Account acc : accList) { acc.Description = 'テスト'; update acc; } |
↑こうではなく、↓こう書く。
1 2 3 4 5 |
List<Account> accList = [SELECT Id, Name, Description FROM Account]; for (Account acc : accList) { acc.Description = 'テスト'; } update accList; |
こうしないとすぐにDMLの発行上限に達してしまいます。
ただ上記の一括DMLの書き方、例えば200件の内1件でも何か問題があってエラーとなってしまった場合、200件全てエラーの扱いとなります。その場合、どのレコードが問題でエラーとなったのか特定するのに苦労します。
いちおうupdateでエラーとなった場合は、対象レコードのIdを返してくれるので特定ができます。
試しにわざとエラーが発生する状態にして、try~catchで例外エラーを拾ってみると
1 2 3 4 5 6 7 8 9 10 11 12 |
List<Account> accList = [SELECT Id, Name, Description FROM Account]; for (Account acc : accList) { acc.Description = 'テスト'; } try { update accList; } catch (Exception e) { System.debug(e.getTypeName()); System.debug(e.getMessage()); System.debug(e.getStackTraceString()); } |
System.DmlException |
このようなエラー情報を取得できます。対象レコードのIdとエラー内容が出力されるため、かなり当たりのつけやすい状態となります。ただしこれは1つ目のエラー情報のため、もし200件の内エラーとなるレコードが2件、3件とある場合、上記エラーを解消しても次は別のエラーが表示されます。これを繰り返していく内にいずれ全てのエラーが解消され一括DMLが処理されるという流れになります。
ちなみにinsertの場合は、まだId発行前のためエラー内容にIdは出力されませんが、代わりにリストの要素番号が出力されます。より開発者寄りのメッセージとなるため、運用者が見てもエラーの特定は難しいでしょう。
System.DmlException Insert failed. First exception on row 1; first error: REQUIRED_FIELD_MISSING, 値を入力してください: [Rank__c]: [Rank__c] AnonymousBlock: line 13, column 1 |
これが、画面のボタンを押したらすぐに処理結果を返してくれるような同期処理で、且つ処理件数が少なければエラー解消までの道のりは短いですが、夜間に処理されるような非同期バッチで、且つ処理件数が膨大な場合、上記のようなログをもとにエラーを解消していくのはなかなか困難です。少しでも解決までの道のりを短くする方法を検討しておいた方が、後々の運用でトラブルを最小限にすることができます。
Databaseクラスを使って一括DMLを発行する(updateの場合)
バッチ処理等でエラーレコードを特定する動きを検討する際、話として挙がりやすいのがデータローダのinsertやupdateと同じ挙動です。どのような挙動かというと
- エラー行以外はinsertやupdateされる
- エラー行は、エラー行ごとにエラー内容が出力される
このような挙動を目指すためには、Databaseクラスを使った一括DMLを行う必要があります。
Database クラス
updateの場合、下記のように書き方になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
List<Account> accList = [SELECT Id, Name, Description FROM Account]; for (Account acc : accList) { acc.Description = 'テスト'; } List<Database.SaveResult> srList = Database.update(accList, false); // エラーレコードの情報出力 for (Integer i = 0; i < accList.size(); i++) { Account acc = accList.get(i); Database.SaveResult sr = srList.get(i); if (!sr.isSuccess()) { System.debug('**** エラー発生:updateが失敗しました ****'); System.debug('取引先Id:' + acc.Id); System.debug('取引先名:' + acc.Name); for (Database.Error err : sr.getErrors()) { System.debug(err.getStatusCode()); System.debug(err.getFields() + ' ' + err.getMessage()); } } } |
まずは6行目が前述した一括updateと最も違う箇所で、Database.updateを使用しています。第2引数にfalseを設定することでエラーが発生しても残りのDMLが行われるようになります。
また前述の一括updateではtry~catchでエラーを取得していましたが、Databaseクラスを使用した場合はエラーの発生も想定内の動きとなるため、Database.updateの戻り値(SaveResultクラス)からエラー内容を取得する書き方となります(8行目~22行目)。
この処理の結果が下記となります。エラー毎に対象レコードの情報とエラー内容が出力されるため、原因の特定がしやすい!
**** エラー発生:updateが失敗しました **** 取引先Id:0010o00002BncBSAAZ 取引先名:University of Arizona REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] **** エラー発生:updateが失敗しました **** 取引先Id:0010o00002BncBQAAZ 取引先名:United Oil & Gas Corp. REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] |
ちなみに8行目以降で、なぜ下記ような書き方をしないのかというと・・
1 2 3 4 5 6 7 8 9 10 11 |
// エラーレコードの情報出力 for (Database.SaveResult sr : srList) { if (!sr.isSuccess()) { System.debug('**** エラー発生:updateが失敗しました ****'); System.debug('取引先Id:' + sr.getId()); for (Database.Error err : sr.getErrors()) { System.debug(err.getStatusCode()); System.debug(err.getFields() + ' ' + err.getMessage()); } } } |
この処理結果はこうなります。
**** エラー発生:updateが失敗しました **** 取引先Id:null REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] **** エラー発生:updateが失敗しました **** 取引先Id:null REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] |
対象の取引先IdをSaveResult.getId()メソッドで取得しようとしていますが、nullになってしまいます。Apex開発者ガイドで確認すると、getId()メソッドはDMLが正常に行われた場合しかIdを返却しない仕様となっているようです。せっかくエラー毎にエラー内容を出力できても対象レコードが分からないと意味がないので、上で書いたような方法でレコード情報を取得する書き方がオススメです。
insertの場合
insertの場合も考え方は同じで、下記のように書くのが良いかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
List<Account> accList = new List<Account>(); Account acc1 = new Account(Name = '◯◯株式会社', Rank__c = 'A'); accList.add(acc1); Account acc2 = new Account(Name = '××株式会社'); accList.add(acc2); Account acc3 = new Account(Name = '△△株式会社', Rank__c = 'B'); accList.add(acc3); Account acc4 = new Account(Name = '□□株式会社'); accList.add(acc4); List<Database.SaveResult> srList = Database.insert(accList, false); // エラーレコードの情報出力 for (Integer i = 0; i < accList.size(); i++) { Account acc = accList.get(i); Database.SaveResult sr = srList.get(i); if (!sr.isSuccess()) { System.debug('**** エラー発生:insertが失敗しました ****'); System.debug('要素No:' + i); System.debug('取引先名:' + acc.Name); for (Database.Error err : sr.getErrors()) { System.debug(err.getStatusCode()); System.debug(err.getFields() + ' ' + err.getMessage()); } } } |
**** エラー発生:insertが失敗しました **** 要素No:1 取引先名:××株式会社 REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] **** エラー発生:insertが失敗しました **** 要素No:3 取引先名:□□株式会社 REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] |
updateのように対象Idを出力することはできませんが、上記のような書き方をすればinsertしようとしたオブジェクトの内容を出力することができるので、エラー原因は特定しやすいとはずです。
1件でもエラーがあればロールバックしたい場合
ここまではデータローダと同じように、エラー以外のレコードはinsert/updateする作りでコードを書いてみましたが、そうではなく1件でもエラーがあればロールバックしたい。でもエラーレコードの特定はしたい。という場合は下記のようなコードになりますかね。参考までに。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
List<Account> accList = [SELECT Id, Name, Description FROM Account]; for (Account acc : accList) { acc.Description = 'テスト'; } // セーブポイントを貼る Savepoint sp = Database.setSavepoint(); List<Database.SaveResult> srList = Database.update(accList, false); // エラーレコードの情報出力 Integer errCnt = 0; for (Integer i = 0; i < accList.size(); i++) { Account acc = accList.get(i); Database.SaveResult sr = srList.get(i); if (!sr.isSuccess()) { errCnt++; //エラーカウント System.debug('**** エラー発生:updateが失敗しました ****'); System.debug('取引先Id:' + acc.Id); System.debug('取引先名:' + acc.Name); for (Database.Error err : sr.getErrors()) { System.debug(err.getStatusCode()); System.debug(err.getFields() + ' ' + err.getMessage()); } } } // ロールバック判定 if (errCnt > 0) { System.debug('エラーが' + errCnt + '件発生したためロールバックします。'); Database.rollback(sp); } |
**** エラー発生:updateが失敗しました **** 取引先Id:0010o00002BncBSAAZ 取引先名:University of Arizona REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] **** エラー発生:updateが失敗しました **** 取引先Id:0010o00002BncBQAAZ 取引先名:United Oil & Gas Corp. REQUIRED_FIELD_MISSING (Rank__c) 値を入力してください: [Rank__c] エラーが2件発生したためロールバックします。 |