Problem
DML exceptions are commonly encountered when running batch jobs due to constant influx of additions and changes to triggers, workflows, and validation rules. These exceptions can cause your batch jobs to automatically abort. Also, it can be almost impossible in large runs to determine the root cause of these exceptions and identify the associated records.
Solution
Provide a simple means to store the DML exceptions and the associated records without the need to write a handler for each DML statement. Add a helper method to create a report of the exceptions and send it via email.
The DMLResults class is a generic collection for all DML Result types (Database.Saveresult, Database.Upsertresult, Database.Deleteresult, Database.Undeleteresult) and provides methods for concatenating the results including mixed Result types.
public class DMLResults {
public integer resultLimit = 1000;
public boolean includeSuccess = false;
public LIST<DMLResults.Result> results {get; private set;}
{ results = new LIST<DMLResults.Result>(); }
/**
* constructor
* @param lst a LIST of [Saveresult | Upsertresult |Deleteresult | Undeleteresult]
* @param records the LIST of sobjects that were included in the dml statement.
*/
public DMLResults() {}
public DMLResults(LIST<object> lst) { this(lst,null);}
public DMLResults(LIST<object> lst, LIST<sobject> records) {
integer cnt=0;
for (Object o : lst) {
try {
Result r;
if (o instanceof Database.Saveresult ) r= new Result((Database.Saveresult) o, records[cnt]);
else if (o instanceof Database.Upsertresult ) r= new Result((Database.Upsertresult) o, records[cnt]);
else if (o instanceof Database.Deleteresult) r= new Result((Database.Deleteresult) o, records[cnt]);
else if (o instanceof Database.Undeleteresult) r= new Result((Database.Undeleteresult) o, records[cnt]);
else
throw new InvalidResultException('Invalid DML Result.');
if (includeSuccess || (!r.success) ) {
if (results.size()<resultLimit) this.add(r);
}
}catch(exception ex) { system.debug(ex); }
cnt++;
}
}
/**
* add additional exceptions
* @param lst a LIST of [Database.Saveresult | Database.Upsertresult | Database.Deleteresult | Database.Undeleteresult]
* @param records the LIST of sobjects that were included in the dml statement.
*/
public void add(LIST<object> lst) { add(new DMLResults(lst,null)); }
public void add(LIST<object> lst, LIST<sobject> records) {
add(new DMLResults(lst,records));
}
public void add(DMLResults.Result r) {
if (results.size()<resultLimit)
results.add(r);
}
public void add(DMLResults dmlr) {
if (results.size()<resultLimit) {
if (results.size()+dmlr.results.size()<resultLimit)
results.addAll(dmlr.results);
else {
for (Result r : dmlr.results) {
if (results.size()<resultLimit)
this.add(r);
else
break;
}
}
}
}
public void add(LIST<DMLResults.Result> lst) {
if (results.size()<resultLimit)
results.addAll(lst);
}
public class Result {
public string record {get;set;}
public Id id {get;set;}
public string errors {get;set;}
public boolean success {get;set;}
//public Database.Error[] errs {get;set;}
//{ errs = new Database.Error[]{}; }
public Result(Database.Saveresult r) { this(r,null); }
public Result(Database.Saveresult r, sobject sObj) {
if (r.getId()!=null)
id=r.getId();
else if (sObj!=null && sObj.id!=null)
id=sObj.Id;
errors=string.valueOf(r);
success=r.isSuccess();
record=(sObj!=null) ? string.valueOf(sObj) : null;
}
public Result(database.Deleteresult r) { this(r,null); }
public Result(database.Deleteresult r,sobject sObj) {
if (r.getId()!=null)
id=r.getId();
else if (sObj!=null && sObj.id!=null)
id=sObj.Id;
errors=string.valueOf(r);
success=r.isSuccess();
record=(sObj!=null) ? string.valueOf(sObj) : null;
}
public Result(database.Upsertresult r) { this(r,null); }
public Result(database.Upsertresult r,sobject sObj) {
if (r.getId()!=null)
id=r.getId();
else if (sObj!=null && sObj.id!=null)
id=sObj.Id;
errors=string.valueOf(r);
success=r.isSuccess();
record=(sObj!=null) ? string.valueOf(sObj) : null;
}
public Result(Database.Undeleteresult r) { this(r,null); }
public Result(Database.Undeleteresult r,sobject sObj) {
if (r.getId()!=null)
id=r.getId();
else if (sObj!=null && sObj.id!=null)
id=sObj.Id;
errors=string.valueOf(r);
success=r.isSuccess();
record=(sObj!=null) ? string.valueOf(sObj) : null;
}
}
public string resultsToString() {
string rtn;
rtn='Total DML results: '+String.valueOf(results.size())+'\n';
if (results.size()>0) {
for(DMLResults.Result r : results) {
if(r.record!=null)
rtn+='Record: '+String.valueOf(r.record)+'\n';
rtn+='Error: '+String.valueOf(r.errors)+'\n\n';
}
}
return rtn;
}
public string resultsToHtml() {
string rtn;
rtn='Total DML results: '+String.valueOf(results.size())+'<br/>';
rtn+='<table border="1px"><tr style="background:gray;"><th>id</th><th>success</th><th>error(s)</th><th>record</hd>';
for(DMLResults.Result r : results) {
rtn+=String.format('{0} {1} {2} {3} ',
new string[]{String.valueOf(r.id),String.valueOf(r.success),r.errors,r.record});
}
rtn+='';
return rtn;
}
public void batchOnFinish(Id jobId) { batchOnFinish(jobId,true); }
public void batchOnFinish(Id jobId, boolean OnlyNotifyOnError) {
boolean html = true;
AsyncApexJob a = [ Select ApexClass.Name, Id, Status, NumberOfErrors, JobItemsProcessed,
TotalJobItems, CreatedBy.Email
From AsyncApexJob where Id =:jobId
];
if (a.NumberOfErrors>0 || results.size()>0 || (!OnlyNotifyOnError)) {
// Send an email to the Apex job's submitter notifying of job completion.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {a.CreatedBy.Email};
mail.setToAddresses(toAddresses);
mail.setSubject(a.ApexClass.Name+': ' + a.Status);
string s = 'The batch Apex job processed '+a.TotalJobItems+' batches with '+a.NumberOfErrors+' failures.';
if(html) {
s+='<br/><br/>';
s+=resultsToHtml();
mail.setHtmlBody(s);
}
else {
s+='\n\n';
s+=resultsToString();
mail.setPlainTextBody(s);
}
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
public class InvalidResultException extends Exception {}
}
Discussion
Here is an simple example that is also a useful batchable class to demonstrate the DMLResult class.
global class batchRefresh implements Database.Batchable<Sobject>, Database.Stateful {
global final string query;
global DMLResults myResults;
global batchRefresh(String q){
Query=q;
}
global Database.QueryLocator start(Database.BatchableContext BC){
myResults = new DMLResults();
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, LIST<SObject> scope){
myResults.add(Database.update(scope,false),scope);
}
global void finish(Database.BatchableContext BC) {
myResults.batchOnFinish(BC.getJobId());
}
}
Executing the statement below will 'refresh' (fire triggers, workflow, and validation rules) all account records. If any DML exceptions are encountered they will be returns via email in a report showing the record ids and the exception.
Database.executeBatch('Select Id from Account',200);
The batch class must implement 'Database.Stateful'. Maintaining state is require to keep track of the all the exceptions for all executions. Keep in mind the size of this state will effect the heap size. To keep the memory footprint and email payload down the DMLResults are defaulted to a limit of 1000 results. This can be overwritten by use of the resultLimit property.
In your DML statement be sure to set the optional opt_allOrNone parameter to false so if a record fails, the remainder of the DML operation can still succeed. For example:
myResults.add(Database.update(scope,false ),scope);
A more complex batch class would typically require you to encapsulate your logic in a fascade class that will return DMLResults in your method(s). For example:
global void execute(Database.BatchableContext BC, LIST<SObject> scope){
myResults.add(MyFascade.doSomething(scope));
}
global class MyFascade {
global DMLResult doSomething(LIST<sobject> sos) {
DMLResult results = new DMLResult();
...
results.add(doSomthingElse());
...
results.add(doFinal());
return results;
}
}
Share