Understanding the Mixed DML Error
A Mixed DML error occurs when you try to perform DML operations on both setup objects (like User, UserRole) and non-setup objects (like Account, Contact) in the same transaction.
This restriction exists in Salesforce because setup objects affect user's record access permissions in the org. When mixing these operations, you might execute operations with incorrect access-level permissions.
Causes of the Mixed DML Error
-
Mixing Setup and Non-Setup Objects: Attempting to insert/update a regular object (like Account) and a setup object (like User) in the same transaction.
-
Batch Class Processing Both Types: When your batch class processes and updates both types of objects within the same execute method.
-
Chained Operations: When your batch class triggers other automation that performs DML on the opposite type of object.
-
Test Methods: Mixed DML errors are especially common in test methods when creating test data for both setup and non-setup objects.
How to Handle the Mixed DML Error
1. Use @future Methods
The most common solution is to separate the transactions using @future methods:
apexpublic class MyBatchClass implements Database.Batchable<SObject> { public Database.QueryLocator start(Database.BatchableContext BC) { return Database.getQueryLocator('SELECT Id, Name FROM Account'); } public void execute(Database.BatchableContext BC, List<SObject> scope) { // Process and update non-setup objects List<Account> accountsToUpdate = new List<Account>(); for(SObject s : scope) { Account acc = (Account)s; acc.Description = 'Updated by batch'; accountsToUpdate.add(acc); } update accountsToUpdate; // Call future method for setup objects if(!accountsToUpdate.isEmpty()) { updateSetupObjects(JSON.serialize(accountsToUpdate)); } } @future public static void updateSetupObjects(String serializedData) { // Deserialize data if needed List<Account> accounts = (List<Account>)JSON.deserialize(serializedData, List<Account>.class); // Perform DML on setup objects User userToUpdate = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId() LIMIT 1]; userToUpdate.Title = 'Updated by batch'; update userToUpdate; } public void finish(Database.BatchableContext BC) { // Perform any post-processing } }
2. Chain Batch Classes
Another approach is to chain batch classes, with one batch processing non-setup objects and another for setup objects:
apexpublic class NonSetupBatch implements Database.Batchable<SObject> { public Database.QueryLocator start(Database.BatchableContext BC) { return Database.getQueryLocator('SELECT Id, Name FROM Account'); } public void execute(Database.BatchableContext BC, List<SObject> scope) { // Process non-setup objects only update scope; } public void finish(Database.BatchableContext BC) { // Start the batch for setup objects Database.executeBatch(new SetupBatch()); } } public class SetupBatch implements Database.Batchable<SObject> { public Database.QueryLocator start(Database.BatchableContext BC) { return Database.getQueryLocator('SELECT Id FROM User LIMIT 10'); } public void execute(Database.BatchableContext BC, List<SObject> scope) { // Process setup objects only List<User> users = (List<User>)scope; // update users } public void finish(Database.BatchableContext BC) { // Completion logic } }
3. Use System.runAs() in Test Classes
For test methods, you can use System.runAs() to isolate setup object operations:
apex@isTest static void testBatchProcess() { // Create and process non-setup objects Account acc = new Account(Name = 'Test Account'); insert acc; // Use runAs for setup objects User u = createTestUser(); System.runAs(u) { // Perform operations on setup objects Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User']; User newUser = new User(/*...user fields...*/); insert newUser; } // Start your batch Test.startTest(); Database.executeBatch(new MyBatchClass()); Test.stopTest(); // Assert results }
Conclusion
Mixed DML errors can be challenging, particularly in batch classes that need to work with both setup and non-setup objects. By separating these operations into different transactions using @future methods, chained batch classes, or System.runAs() for tests, you can effectively avoid these errors.
Always identify which objects are setup objects first, then design your batch processing patterns to handle them in separate transactions.