[SalesForce] Transaction Control for multiple batches

Is it possible to implement transactional DML for batches?
for example we have parent object and child and if the update of child object is failed then the update of this particular parnet and child should be rolled back but the rest parent-child objects should be saved if they are updated without errors. But the difficulty is that there are chunks of such records: 10000 parents and each parent have 2 child. Is it possible to leverage System.Savepoint in clever way to support batch update

Best Answer

When you perform an update, and that update performs additional updates via a trigger, you can partially fail the transaction to perform an automatic rollback and partial retry.

Consider the following trigger:

trigger cascadePhone on Account (after update) {
    // List of numbers that were changed from
    Set<String> oldPhones = new Set<String>();

    // Find all phones that changed and record those
    for(Account record: Trigger.old) {
        if(record.Phone != Trigger.newMap.get(record.Id).Phone) {
            oldPhones.add(record.Phone);
        }
    }

    // Find all contacts that might match
    Contact[] contacts = [SELECT    Id, AccountId, Phone 
                          FROM      Contact 
                          WHERE     AccountId IN :Trigger.new AND 
                                    Phone IN :oldPhones];

    // Remove contacts that have the right phone but wrong account
    // We count backwards because we're removing records in place in the list
    // and if we counted forwards we'd skip rows
    for(Integer index = contacts.size()-1; index >= 0; index--) {
        if(contacts[index].Phone == Trigger.oldMap.get(contacts[index].AccountId).Phone) {
            contacts[index].Phone = Trigger.newMap.get(contacts[index].AccountId).Phone;
        } else {
            contacts.remove(index);
        }
    }

    // Partial update
    Database.SaveResult[] results = Database.update(contacts, false);

    // For each record that failed, mark the account as failed as well
    for(Integer index = 0; index < contacts.size(); index++) {
        if(!results[index].isSuccess()) {
            Trigger.newMap.get(contacts[index].AccountId).Phone.addError('Failed to update some phone numbers');
        }
    }

}

If you perform an update of accounts in an execute function, like this:

public static void execute(Database.BatchableContext bc, Account[] scope) {
    // Fix accounts in scope
    // Then update them
    Database.update(scope, false);

}

What's not obvious here is that the system automatically performs a rollback on all records that error out, and attempt to retry the accounts that are left. This is an implied partial callback. Nowhere am I using a SavePoint to rollback the errors; the system handles the logic for me. The child records that failed also roll back, so the contacts that were on the accounts that failed would also not update as a result of this code.