DML and Transactions · Lesson 21 of 41
Database Methods and Partial Success in Apex
A plain insert is all-or-nothing: if one record in 200 fails a validation rule, all 200 are rolled back and an exception is thrown. That's right for a single business operation, but wrong for a data load where you want the 199 good records saved and the bad one reported. The Database class methods give you that choice.
In this lesson
- DML statements vs Database methods
- allOrNone = false and SaveResult
- Reading errors per record
- DMLOptions
- Saving in user mode
DML statement vs Database method
insert records; | Database.insert(records, false); | |
|---|---|---|
| One record fails | everything rolls back, exception thrown | the others are saved |
| Errors | a DmlException to catch | a result object per record |
| Use for | one logical operation that must fully succeed | imports, integrations, batch jobs |
With allOrNone = true (the default if you omit it), Database methods behave like the statements.
Partial success with SaveResult
List<Contact> rows = new List<Contact>{
new Contact(LastName = 'Verma', Email = 'asha@acme.example'),
new Contact(FirstName = 'Missing'), // no LastName
new Contact(LastName = 'Kumar', Email = 'not an email'), // invalid email
new Contact(LastName = 'Rao')
};
List<Database.SaveResult> results = Database.insert(rows, false);
Integer ok = 0;
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult sr = results[i];
if (sr.isSuccess()) {
ok++;
} else {
for (Database.Error err : sr.getErrors()) {
System.debug('Row ' + i + ' failed: ' + err.getStatusCode() + ' – ' + err.getMessage() + ' ' + err.getFields());
}
}
}
System.debug(ok + ' of ' + rows.size() + ' saved');
System.debug('In the database: ' + [SELECT COUNT() FROM Contact]);
Row 1 failed: REQUIRED_FIELD_MISSING – Required fields are missing: [LastName] (LastName) Row 2 failed: INVALID_EMAIL_ADDRESS – Email: invalid email address: not an email (Email) 2 of 4 saved In the database: 2
Results come back in the same order as the input list, so results[i] belongs to rows[i]. That's how you report which source row failed – essential for imports, where users need to fix and resend specific lines.
A reusable pattern: collect failures
// DmlResultReport.cls
public with sharing class DmlResultReport {
public Integer succeeded = 0;
public List<String> failures = new List<String>();
public DmlResultReport(List<SObject> records, List<Database.SaveResult> results, Schema.SObjectField labelField) {
for (Integer i = 0; i < results.size(); i++) {
if (results[i].isSuccess()) {
succeeded++;
continue;
}
List<String> messages = new List<String>();
for (Database.Error e : results[i].getErrors()) messages.add(e.getMessage());
failures.add(records[i].get(labelField) + ': ' + String.join(messages, '; '));
}
}
}
List<Account> load = new List<Account>{
new Account(Name = 'Acme Corp', Website = 'acme.example'),
new Account(Website = 'no-name.example'), // Name is required
new Account(Name = 'Globex', Website = 'globex.example')
};
DmlResultReport report = new DmlResultReport(load, Database.insert(load, false), Account.Website);
System.debug('Saved: ' + report.succeeded);
for (String f : report.failures) System.debug('Failed: ' + f);
Saved: 2 Failed: no-name.example: Required fields are missing: [Name]
DMLOptions
Database.DMLOptions changes how a save behaves – truncating long text instead of failing, running assignment rules, or allowing saves that duplicate rules would block:
Database.DMLOptions opts = new Database.DMLOptions();
opts.allowFieldTruncation = true; // cut long text to fit instead of failing
opts.optAllOrNone = false;
Account longName = new Account(Name = 'X'.repeat(300));
Database.SaveResult sr = Database.insert(longName, opts);
System.debug('Saved with truncation: ' + sr.isSuccess());
System.debug('Stored length: ' + [SELECT Name FROM Account WHERE Id = :sr.getId()].Name.length());
// other common options
// opts.assignmentRuleHeader.useDefaultRule = true; // Leads and Cases
// opts.duplicateRuleHeader.allowSave = true; // bypass "alert" duplicate rules
// opts.emailHeader.triggerUserEmail = true; // send standard emails (e.g. case comments)
Saved with truncation: true Stored length: 255
Saving in user mode
Apex runs in system mode by default: object and field permissions aren't checked. When code acts on behalf of a user – an LWC controller, a REST endpoint – you usually want the user's permissions enforced. Add as user to a DML statement, or pass AccessLevel.USER_MODE to a Database method:
insert as user new Account(Name = 'Saved with the user\'s permissions');
List<Database.SaveResult> res = Database.insert(
new List<Account>{ new Account(Name = 'Also user mode') }, false, AccessLevel.USER_MODE);
System.debug('User-mode insert ok: ' + res[0].isSuccess());
System.debug('Accounts: ' + [SELECT COUNT() FROM Account WITH USER_MODE]);
User-mode insert ok: true Accounts: 2
If the user lacks create access to the object or edit access to a field you set, a user-mode save fails with a clear error instead of silently writing data they shouldn't be able to write. The security lesson covers this in depth.
Practice
Try it yourself
A Queueable updates 5,000 Opportunities and must not stop because a handful fail validation rules. Which call do you use, and what do you do with failures?
Show answer
Database.update(opps, false)(in chunks within limits), then loop over the SaveResults and log failures – for example to a custom log object or a Platform Event – with the record Id and error messages.When should you keep the default all-or-nothing behaviour?
Show answer
When the records form one business operation – an order and its lines, a case and its first comment. Saving half of it would leave inconsistent data.
Quick check
What does the second parameter of Database.insert control?
allOrNone – false allows partial success.
How do you match a SaveResult to its record?
By index – results are in the same order as the input list.
How do you insert while enforcing the user's permissions?
insert as user …, or Database.insert(records, AccessLevel.USER_MODE).
Summary
- Use Database methods with allOrNone=false for imports and integrations.
- Read SaveResult/UpsertResult/DeleteResult by index and report each failure.
- DMLOptions controls truncation, assignment rules and duplicate handling.
- Use as user or AccessLevel.USER_MODE when acting for a user.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.