Instance field holds Database operation results
ID |
apex.correctness.stateful_db_result |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Resource Management |
Language |
Apex |
Description
Reports an instance field typed Database.SaveResult, UpsertResult, DeleteResult or a collection of them.
Rationale
A Database.*Result is not a small status code: it carries the errors raised for the row it describes, and each error holds the field names and the record it came from. Parked in an instance field of a Database.Stateful batch, the whole set is serialized between chunks and grows with every batch that fails, until the job dies on the serialization limit with a message that names the batch rather than the field filling it. The same field in an ordinary class keeps records alive for the length of the transaction and pushes it toward the heap limit.
The following code illustrates the pattern detected by this rule:
global class OrderLoadBatch implements Database.Batchable<SObject>, Database.Stateful {
// FLAGGED: Instance field holds Database operation results
private List<Database.SaveResult> saveResults = new List<Database.SaveResult>();
// FLAGGED: Instance field holds Database operation results
global Database.UpsertResult[] upsertResults;
// FLAGGED: Instance field holds Database operation results
public Database.DeleteResult lastDelete;
private Integer failureCount = 0;
private List<Id> failedIds = new List<Id>();
private static final String QUERY = 'SELECT Id FROM Order';
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(QUERY);
}
global void execute(Database.BatchableContext bc, List<SObject> scope) {
List<Database.SaveResult> results = Database.update(scope, false);
for (Database.SaveResult r : results) {
if (!r.isSuccess()) {
failureCount++;
}
}
}
global void finish(Database.BatchableContext bc) {
System.debug(failureCount);
}
}
Remediation
Consume the results where they are produced: loop over them immediately, keep only what the rest of the job needs - a failure count, the failed ids, a message string - and let the results go out of scope. Where the detail must survive the transaction, write it to a custom logging object or publish a platform event, which outlives a rollback.