0

Correct me if my understanding is wrong -

All the DML operations are committed only if the entire code runs successfully.

I've carried out the following activity:

Written a block of code which will perform following actions through DML statements: 1.) Update all the case records (I've 35 records) - change the picklist Reason to "Installation"

2.) Update all the opportunities to have their names appended with String.valueOf(System.now()) except for one opportunity record - For this record, restricted picklist value will be set to a random value to intentionally cause an error.

This block of code will be fired whenever a contact is created i.e. only if Trigger.isInsert and Trigger.isAfter are true.

My expectation: The contact record creation would fail due to bad picklist value for the opportunity record, case update will not be committed as well.

What happened: The contact record saved successfully. All the cases were updated to have Reason as "Installation". None of the opportunities records were updated.

Can anyone please explain me why this happened? Is the same behaviour expected if I was supposed to update the related records. For instance: updating all child records via trigger when parent is updated? Will parent update fail, or will it succeed?

Thanks in advance

  • Do you have try catch block? – Pranay Jaiswal Mar 2 '19 at 9:15
  • Hi @Pranay, Yes - both case and opportunity DMLs are in try catch. Even caught the exception: USER_DEBUG [88]|DEBUG|ERROR: Update failed. First exception on row 207 with id 0066F00000zGW5IQAW; first error: INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST, DB Competitor: bad value for restricted picklist field: fgdfgfgre: [DB_Competitor__c] – sfdcnewbie Mar 2 '19 at 9:56
  • Okay I just tried out following activities: 1.) Trigger (both after and before update) on Master object would update all of its related child objects thru DML whenever the master object is updated. One child record update to cause failure intentionally. Result: Master updated. Children failed to update. 2.) Trigger( Before and After Insert) on Master Object would update all Child object records (related to other master records). One child update to cause failure intentionally. Result: New Master record created. All children update failed. So it seems there are two different transactions. – sfdcnewbie Mar 2 '19 at 10:46
1

There are two ways you can allow for partial failures: by catching the DMLException, or by using the allOrNone flag to allow partial success. Many code examples you might find on the Internet will typically look like this:

try {
  update cases;
  update opportunities;
} catch(Exception e) {
  System.debug(e.getMessage());
}

You should never use this code pattern. It allows updates to be committed when they should not be. In your example, using the above code, the exception is not handled properly, and the transaction succeeds.

The correct solution depends on the situation at hand. Typically, if you need the transaction to be atomic (meaning an all-or-none operation), you would either not catch the exception, or you would rollback the transaction. Not catching the exception is usually a bad idea, as it can cause a Visualforce page to lose its view state and display an generic error.

This is the typical minimum recommended solution:

SavePoint sp = Database.setSavePoint();
try {
  update cases;
  update opportunities;
} catch(Exception e) {
  Database.rollback(sp); // Reset back to before updates
}

Note that in Visualforce, the errors will automatically be rendered to the appropriate records/fields as well, if displayed on the page, otherwise you'd write your own error (via Apexpages.addMessage).

However, the framework allows partial success. This is typically used to allow bulk updates in triggers in the presence of errors.

For example, if you had a case trigger that updated the account's opportunities, you might want to allow for partial success. Such a trigger would look like this:

trigger updateOpps on Case(after update) {
  Set<Id> accountIds = new Set<Id>();
  for(Case record: Trigger.new) {
    accountIds.add(record.AccountId);
  }
  Opportunity[] updates = [SELECT AccountId FROM Opportunity WHERE AccountId = :accountIds];
  Map<Id, String> errors = new Map<Id, String>();
  for(Opportunity record: updates) {
    record.SomeField__c = Math.random()*1000;
  }
  Database.SaveResult[] results = Database.update(updates, false);
  for(Integer i = 0, s = results.size(); i < s; i++) {
    Database.SaveResult result = results[i];
    if(!result.isSuccess()) {
      errors.put(records[i].AccountId, result.getErrors()[0].getMessage());
    }
  }
  for(Case record: Trigger.new) {
    String message = errors.get(record.AccountId);
    if(message != null) {
      record.AccountId.addError('Failed to update related record. The first error was:' + message);
    }
  }
}

This pattern will allow errors to be displayed on a case-by-case basis (no pun intended). The code calling this trigger can choose to use allOrNone to abort the entire transaction, or allow partial success.

The key point here is that if the transaction completes successfully (i.e. the transaction reaches the last line of code and returns without throwing an exception), the commit will occur. This is why it is vital that you either do not catch exceptions, or handle exceptions correctly. Never use the try-catch-debug pattern, and you should be okay.

  • Thanks - That just explains everything I needed to know! – sfdcnewbie Mar 2 '19 at 16:46
  • @sfdcnewbie glad to help. Just follow the recommendations, you'll be fine. – sfdcfox Mar 2 '19 at 17:02
1

The behavior you are getting because the update of Parent and update of Children are not in a single transaction. To make the piece of code as transactional, it will be like this:

Savepoint sp = Database.setSavepoint();
try
{
    //update contact records        
    update lstContact; //update all contacts

    //update Opportunity records    
    update lstOpportunities;

}catch (Exception ex)
{
    Database.rollback(sp);
    throw new CustomException (ex);
}

For any errors either from Parent record update or Child Opportunity update, it will enter in the catch block and Database.rollback(sp) will entirely rollback the transaction.

For more information, refer Transaction Control

Other scenario could be possible:

If your first operation, Parent record update is initiated from Contact trigger which intern update Opportunity then everything will be in transactional mode. So, all the Contact & Opportunity operations will be performed in single transaction and if any error occurs everything will be rolled back. Your case is different here.

  • I think you meant transactional, not transnational. – sfdcfox Mar 2 '19 at 12:25
  • Sorry for my typo, you are correct – Santanu Boral Mar 3 '19 at 4:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.