[SalesForce] Batch, future and MIXED_DML_OPERATION

One of the requirements for out project is to do some complex logic on both setup and non-setup objects.

In short, whenever a field on Account is edited, we should recalculate our custom sharing for accounts, opportunities, leads and some custom objects. After that is done, all users associated with this account should be placed in a specified public group.

What we did first was a trigger, which failed the test with high volumes(Too many DML rows) because there are tens of thousands records that need re-sharing for a single account.

The second attempt is to use batch, but here's what happened:

  1. Re-sharing goes well(AccountShare, LeadShare, OpportunityShare, CustomObject__share records are deleted/inserted properly)
  2. Moving users between public groups doesn't work:
    1. By trying to call the method that did it(from the batch itself), we got First error: Future method cannot be called from a future or batch method
    2. Then we changed this method so that it's not @future anymore, but this raised the following error: First exception on row 0; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): GroupMember, original object: AccountShare
    3. Finally, we modified the finish() method of the batch so that it updates these account records and we modified we trigger that it can catch this update and put the users in the correct public group, but the error we got was: First error: Too many future calls: 1

Between each of the steps we were "rolling back" the code.

So the question is:
How can we manipulate both setup and non-setup objects in a batch?


The class looks like this(this is not the actual code, it's heavily stripped for readability purposes):

global class BATCH_Dealer_Changed_Share implements Database.Batchable<sObject> {

    Map<Id, Account> dealerAccs;

    global BATCH_Dealer_Changed_Share() {
        // 
        dealerAccs = new Map<Id, Account>([SELECT Id //and other fields
                                            FROM Account 
                                            WHERE RecordTypeId = :UTIL_RecordTypes.AccountDealerRecordTypeId
                                            AND New_Share__c != null]);
    }

    global Iterable<SObject> start(Database.BatchableContext BC) {
        Iterable<SObject> myIter = (Iterable<SObject>) new ITER_Changing_Consolidation(dealerAccs.values()); 
        // This iterable just returns me all the Share records(AccountShare, LeadShare, etc.) in one list
        return myIter;
    }

    global void execute(Database.BatchableContext BC, List<SObject> scope) {
        // a few hundred lines of complex logic for all share records
        // Attempts 1 and 2 were here
        try {
            insert newShares;
            delete oldShares;
            // And also tried calling the method to move users here(attempts 1 and 2)
        } catch(Exception e) {
            // logging exception here if needed
        }

    }

    global void finish(Database.BatchableContext BC) {
        for (Account a : dealerAccs.values()) {
            a.Share__c = a.New_Share__c;
            a.New_Share__c = null;
        }
        update dealerAccs.values();
    }
    // Also tried to call that method here, but had 'Too many future calls: 1' error
}

Best Answer

The basic definition is Setup and Non-Setup objects can not be updated together in single transaction. Asynchronous transaction can not invoke other asynchronous transaction.

However, one of the approach which can be scaled with this existing solution is "Batch chaining"

As yo have already updated the Setup object in execute() method. Now just keep that information in stateful batch.

Pass that information in a new Batch which would be invoked from finish() method proving all Ids of accounts which require new sharing update. Don't form such bulk records in finish.

In chained batch, you can control size of batch to manage future calls limit.

Note: This would increase process time but solution will become feasible to any number of records.

Related Topic