[Salesforce] エラーが出てもエラーレコード以外は更新させる

salesforceでインサート・アップデートする場合、ガバナ制限の関係から都度DMLを発行せずにある程度まとめてやる事が多い。
その際に、一件でもエラーが出てしまうと他もまきこまれてロールバックされてしまうので、エラー以外は更新させる方法。

通常の方法

通常の方法、というか、簡易な方法としては、以下のようにインサートする。

1
2
3
4
5
List<Lead> lists = new List<Lead>();
for(xx){
  //Leadを作ってaddする処理
}
insert lists;

for文の中でinsertせずに、リストにまとめておいて外で一回だけinsertする。
これで、DMLは一回だけ発行したことになるので、節約が可能。
ループの中ではSOQL、DMLはしない。これがapexの鉄則。

しかしこれだと例えば全部で200件インサートする予定があって、そのうち150件目に不備がありエラーが出た場合、 そこで処理がストップし、それまでにインサートした149件もすべてロールバックされインサート自体がなかったことになる。 要件によってはこの方がいい場合もあるが、エラーが出たレコード以外は全てインサートしたい場合もある。

Database Class

そんな時は、Databaseクラスを使う。
使い方は、Database.insert()とするだけ。

このメソッドの第1引数に、インサートするオブジェクトのリストを、 第2引数に、エラー時の挙動を入れる。

  • trueの場合はエラー時には全てロールバック(上記の単純なinsertと同じ)(デフォルト)
  • falseの場合はエラーレコード以外はインサートする

上記の例を書き換えると、以下のようになる。

1
2
3
4
5
List<Lead> lists = new List<Lead>();
for(xx){
  //Leadを作ってaddする処理
}
Database.insert(lists, false);

これでエラーのレコード以外はインサートさせる事が出来た。
updataupsertdelete についても同じ。

Database クラス

Upsert

upsertも上記と方法は同じだが、upsertでは外部IDの指定が可能になるが、その方法が少し簡易バージョンとは違った。

例えば、Leadオブジェクトの、カスタム項目、UserID__cを外部IDとして指定する場合。
簡易バージョンの場合は以下のようになる。

1
2
3
List<Lead> lists = new List<Lead>();
//処理中略
upsert lists UserID__c;

上記、Databaseを使ったパターンだと、ドキュメントには以下のように書かれている。

1
upsert(recordToUpsert, externalIdField, allOrNone)

この第二引数のexternalIdFieldが外部IDの指定箇所になるが、型は、Schema.SObjectFieldとなっていて、 そのまま Database.upsert(lists, UserID__c, false)とやるとエラーが出る。

なので以下のようにして指定したい項目のSchema.SObjectFieldを取得して指定してやる必要がある。

1
2
3
4
5
List<Lead> lists = new List<Lead>();
//処理中略
Schema.DescribeFieldResult F = Lead. UserID__c.getDescribe();
Schema.sObjectField T = F.getSObjectField();
Database.upsert(lists, T, false);

これで外部IDを指定したアップサートが可能となる。

SObjectField クラス

エラーハンドリング

さて、DMLでエラーが発生した場合、その内容を取得するには、try..catchで例外を拾っていた。

1
2
3
4
5
6
try{
  insert lists;
}catch(DmlException e){
  System.debug(e.getMessage());
  System.debug(e.getStackTraceString());
}

Exception クラスおよび組み込み例外

しかし、Databaseを使ってinsertなどをした場合は別の方法で取得する。
というか、エラーだけでなく、結果を一件ずつ取得することが出来るため、それらからエラー分を取得する、という形になる。

SaveResultクラスを使用する。

1
List<Database.SaveResult> res_lists = Database.insert(lists, false);

取得した結果から、成功、失敗を取得し、内容を取得したりする。

1
2
3
4
5
6
7
8
9
10
11
12
List<Database.SaveResult> res_lists = Database.insert(lists, false);
for (Database.SaveResult res : res_lists) {
  if(res.isSuccess()){
      //成功時
  }else{
      //失敗時
      for(Database.Error err : res.getErrors()) {
          System.debug(err.getStatusCode());
          System.debug(err.getMessage());
      }
  }
}

例えば出力内容は以下のような感じ。

1
2
DEBUG|REQUIRED_FIELD_MISSING
値を入力してください: [LastName]

この SaveResult は、インサート時の結果を取得するためのクラスで、 インサート、アップサート、アップサート、デリート、それぞれのクラスが別々に用意されている。

  • insert – SaveResult
  • update – UndeleteResult
  • upsert – UpsertResult
  • delete – DeleteResult

詳しくは、Apex 開発者ガイド、を参照。
(直接のリンクがなぜか貼れなかった)

Apex開発者ガイド – リファレンス – Database名前空間

まとめ

以上で一通り想定していた動作をさせることができた。
エラー時に他をロールバックするかどうかとかは、結構忘れがちになるけれど、結構難しい問題。
仕様策定時にきちんと考慮していきたい。

   このエントリーをはてなブックマークに追加