7.3 DML Operations & Bulkification
Key Takeaways
- Standalone DML (insert/update/upsert/delete/undelete) is all-or-none by default; one failure rolls back that DML operation’s work for the statement
- Database.insert/update/upsert/delete/undelete methods accept allOrNone; false enables partial success with SaveResult arrays
- Bulkify by collecting sObjects in lists outside loops and performing one DML per object type (or few batched statements)—never DML or SOQL inside per-record loops
- upsert can match on Id or an External ID field to insert-or-update integration rows cleanly
- DmlException and SaveResult/Error objects expose status codes and fields for handling and user messaging
7.3 DML Operations & Bulkification
Quick Answer: Apex DML persists sObjects: insert, update, upsert, delete, and undelete. Standalone DML is all-or-none.
Database.*methods can setallOrNone = falsefor partial success and return SaveResult (or DeleteResult/UpsertResult) arrays. Bulkification means gather records in collections, query once, DML once—never SOQL/DML inside tight per-record loops. Upsert with an External ID is the integration workhorse.
This section sits at the center of trigger and service design. Platform Developer I constantly tests whether you create 200 DML statements or one.
The Five DML Operations
| Operation | Effect |
|---|---|
| insert | Create new rows; platform assigns Id on success |
| update | Modify existing rows; Id required |
| upsert | Insert or update based on Id or External ID match |
| delete | Soft-delete to Recycle Bin (most objects) |
| undelete | Restore from Recycle Bin |
Account a = new Account(Name = 'Acme West');
insert a; // a.Id now populated
a.Name = 'Acme West HQ';
update a;
delete a;
undelete a;
Lists work the same way and are the default bulk form:
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(LastName = 'Primary', AccountId = acc.Id));
}
insert contacts; // one DML statement for many rows
Row limits vs statement limits: One insert contacts with 200 rows is one DML statement and 200 DML rows. Two hundred insert c; calls inside a loop are 200 DML statements—and will fail governors long before business logic finishes (see 7.4).
Standalone DML vs Database Methods
Standalone (all-or-none)
try {
insert accounts;
} catch (DmlException e) {
// Entire insert list fails if any row fails
System.debug(e.getMessage());
System.debug(e.getDmlFieldNames(0));
System.debug(e.getDmlStatusCode(0));
}
If any record in the list fails validation, required fields, or duplicates (depending on rules), the whole standalone DML operation fails and throws DmlException (transaction partial rollback behavior depends on savepoints and surrounding context—but the statement does not partially apply successes the way allOrNone=false does).
Database methods (optional partial success)
List<Database.SaveResult> results = Database.insert(accounts, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error err : results[i].getErrors()) {
System.debug(err.getStatusCode() + ': ' + err.getMessage());
System.debug(err.getFields());
}
} else {
System.debug('Inserted Id: ' + results[i].getId());
}
}
| API | Returns | allOrNone |
|---|---|---|
Database.insert(list, allOrNone) | SaveResult[] | true (default overload) / false |
Database.update | SaveResult[] | same |
Database.upsert | UpsertResult[] | same |
Database.delete | DeleteResult[] | same |
Database.undelete | UndeleteResult[] | same |
When partial success helps: data loads, integration batches, “save what you can and report errors.” When all-or-none is better: multi-object business transactions that must not leave orphan children (often combined with Savepoints for multi-step rollback).
System.Savepoint sp = Database.setSavepoint();
try {
insert parents;
insert children;
} catch (DmlException e) {
Database.rollback(sp);
throw e;
}
Bulk Patterns: Collect Outside Loops
Anti-pattern (fails exams and production)
// BAD: SOQL + DML inside loop
for (Account a : Trigger.new) {
List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
for (Contact c : cons) {
c.Description = 'Synced';
update c; // DML per contact!
}
}
Bulk-safe pattern
// GOOD: one SOQL, in-memory work, one DML
Set<Id> accountIds = new Set<Id>();
for (Account a : Trigger.new) {
accountIds.add(a.Id);
}
List<Contact> cons = [
SELECT Id, AccountId, Description
FROM Contact
WHERE AccountId IN :accountIds
];
for (Contact c : cons) {
c.Description = 'Synced';
}
update cons;
Rules of thumb:
- No SOQL inside loops over trigger batches or unpredictable sizes.
- No DML inside loops that could run per record—accumulate
List<SObject>/ maps, then DML. - Use Maps keyed by Id for O(1) parent lookups after a single query.
- Split lists by object type; mixed sObject DML has special rules—prefer homogeneous lists.
- Guard empty lists (
if (!toUpdate.isEmpty()) update toUpdate;) for clarity and to avoid useless statements.
Map-driven parent/child updates
Map<Id, Account> newMap = (Map<Id, Account>)Trigger.newMap;
List<Opportunity> opps = [
SELECT Id, AccountId, Name
FROM Opportunity
WHERE AccountId IN :newMap.keySet()
];
for (Opportunity o : opps) {
Account parent = newMap.get(o.AccountId);
if (parent != null && parent.Name != null) {
o.Name = parent.Name + ' – Renewal';
}
}
update opps;
Upsert and External IDs
Upsert inserts when no match exists and updates when a match is found:
// Match on Salesforce Id when present
upsert accounts;
// Match on custom External ID field (API name ends with __c, marked External ID / Unique as designed)
List<Product__c> products = new List<Product__c>();
products.add(new Product__c(SKU__c = 'SKU-1', Name = 'Widget'));
products.add(new Product__c(SKU__c = 'SKU-2', Name = 'Gadget'));
upsert products Product__c.Fields.SKU__c;
// or: Database.upsert(products, Product__c.SKU__c, false);
Exam uses: middleware sends external keys; Salesforce must not create duplicates. External ID fields enable matching without a prior SOQL to resolve Salesforce Ids (still query when you need extra fields). Unique External IDs prevent ambiguous matches; non-unique external IDs can cause upsert errors when multiple rows match.
Reading Failures: DmlException and Results
DmlException helpers (standalone path):
getNumDml()— how many row-level errorsgetDmlIndex(i)— index into the original listgetDmlId(i),getDmlMessage(i),getDmlStatusCode(i),getDmlFieldNames(i),getDmlType(i)
Database.Error (partial success path): getMessage(), getStatusCode(), getFields().
Status codes you will reason about include required-field failures, validation-rule failures, and duplicate rules—exam questions may ask which API surfaces field names for UI error display.
Related Operations (Awareness)
merge— combine duplicate leads/contacts/accounts (specialized DML).convertLead— Database.LeadConvert patterns (often separate exam topics).- Recursive DML — updates that re-enter triggers require static recursion guards (covered deeply in the triggers chapter); bulkification alone does not stop recursion.
Checklist Before You Submit Code on the Exam
- Count DML statements in the worst-case batch of 200.
- Confirm lists are built outside inner loops.
- Choose standalone vs
Database.*based on all-or-none vs partial success. - For integrations, prefer upsert + External ID over blind insert.
- Handle DmlException or iterate SaveResult so errors are not silent.
Bulk-safe DML is how you stay under the 150 DML statements and 10,000 DML rows synchronous ceilings described next—and how real multi-tenant apps stay stable under load.
What is the main behavioral difference between insert accounts; and Database.insert(accounts, false);?
Which pattern correctly bulkifies DML when creating one Contact per Account in Trigger.new after insert?
Why do integrations often upsert using an External ID field?