Custom Save actions in Apex are tricky!
Here are my requirements which I was unable to fulfill without harming the stability of my code:
- Save for New and Edit (works with record with and without Id)
- Saves a parent and related child records in one go
- Provides ACID properties. If single DML fails all other will be rolled back
- Displays DML errors as nice Page messages (no white exception screen)
1. Straight forward solution => Ugly exception instead of page messages
public void doSave() {
upsert parent;
for(Child__c child : children) {
if(child.Id == null) {
child.mdr_Parent__c = parent.Id;
}
}
upsert children;
}
2. Convert exception to messages => No ACID properties
public PageReference doSave() {
// Note: Without try/catch DML errors would be shown as ugly white error page
try {
upsert parent;
for(Child__c child : children) {
if(child.Id == null) {
child.mdr_Parent__c = parent.Id;
}
}
upsert children;
}
catch(Exception ex) {
ApexPages.addMessages(ex);
}
PageReference result = (ApexPages.hasMessages()) ? null : new PageReference('/' + parent.Id);
return result;
}
3. Add Database.rollback() => Instable, side effects, errors
The most elaborate solution leads to Data not available and Duplicate values errors under certain circumstances.
public PageReference doSave() {
// Note: Required as below try/catch would prevent the platforms default behaviour to rollback on error
Savepoint toBeforeState = Database.setSavepoint();
// Note: Without try/catch DML errors would be shown as ugly white error page
try {
upsert parent;
for(Child__c child : children) {
if(child.Id == null) {
child.mdr_Parent__c = parent.Id;
}
}
upsert children;
}
catch(Exception ex) {
ApexPages.addMessages(ex);
Database.rollback(toBeforeState);
// Note: Workaround for https://salesforce.stackexchange.com/questions/57020/data-not-available-when-using-database-rollback-in-custom-save-action
removeInvalidIds();
}
PageReference result = (ApexPages.hasMessages()) ? null : new PageReference('/' + parent.Id);
return result;
}
4. The perfect solution
public PageReference doSave() {
...
}
Database.rollback(..)
is invoked and hence partial update ID fields may be still present on the VF page/controller when the user clicks Save the second time after an error