Recipes by Category

App Distribution (2) Bundle logic, interface and services for distribution. App Logic (37) The Apex programming language, workflow and formulas for logic. Collaboration (5) The Salesforce Chatter collaboration platform. Database (29) Data persistence, reporting and analytics. Integration (33) Web Service APIs and toolkits for integration. Security (9) Platform, application and data security. Tools (4) Force.com tooling User Interface (36) Visualforce MVC and metadata-drive user interfaces. Web Sites (12) Public web sites and apps with optional user registration and login.
Beta Feedback
Cookbook Home » Using Batch Apex to Reassign Account Owners

Using Batch Apex to Reassign Account Owners

Post by Developer Force  (2010-07-16)

Status: Certified
Level: novice

Problem

You want to reassign accounts from one owner to another. However, you have 100,000 accounts, which are too many to use a standard Apex trigger or class in a single transaction.

Solution

Use batch Apex to process all the records at once.

To use batch Apex, you must write a class that implements the Database.Batchable interface provided by Salesforce.com.

After you write the class, you should create a Visualforce page for executing the class. You could also create a button that calls a Visualforce controller. It is a best practice to execute a batch job from Visualforce.

You can only have five queued or active batch jobs at one time. Use extreme care if you are planning to invoke a batch job from a trigger. You must be able to guarantee that the trigger will not add more batch jobs than the five that are allowed. In particular, consider API bulk updates, import wizards, mass record changes through the user interface, and all cases where more than one record can be updated at a time.

The following is the entire code example. In the Discussion, we include more explanation of the various pieces that must be included in the class.

global class AccountOwnerReassignment implements 
             Database.Batchable<SObject>, Database.Stateful{
    
    User fromUser{get; set;}
    User toUser{get; set;}
    Double failedUpdates{get; set;}
 
    global AccountOwnerReassignment(User fromUser, User toUser){
        this.fromUser = fromUser;
        this.toUser = toUser;
        failedUpdates = 0;
    }
    
    global Database.queryLocator 
                    start(Database.BatchableContext ctx){
        return Database.getQueryLocator([SELECT id, name, ownerId 
                        FROM Account WHERE ownerId = :fromUser.id]);
    }
    
    global void execute(Database.BatchableContext ctx, List<Sobject>
                        scope){
        List<Account> accs = (List<Account>)scope;
        
        for(Integer i = 0; i < accs.size(); i++){
            accs[i].ownerId = toUser.id;
        }
        
        List<Database.SaveResult> dsrs = Database.update(accs, false);
        for(Database.SaveResult dsr : dsrs){
            if(!dsr.isSuccess()){
                failedUpdates++;
            }
            
        } 
    }
    
    global void finish(Database.BatchableContext ctx){
    
        AsyncApexJob a = [SELECT id, ApexClassId, 
                       JobItemsProcessed, TotalJobItems, 
                       NumberOfErrors, CreatedBy.Email 
                       FROM AsyncApexJob 
                       WHERE id = :ctx.getJobId()];
        
        String emailMessage = 'Your batch job '
             + 'AccountOwnerReassignment '
             + 'has finished.  It executed ' 
             + a.totalJobItems 
             + ' batches.  Of which, ' + a.jobitemsprocessed 
             + ' processed without any exceptions thrown and ' 
             + a.numberOfErrors +
             ' batches threw unhandled exceptions.'
             + '  Of the batches that executed without error, ' 
             + failedUpdates 
             + ' records were not updated successfully.';
        
        Messaging.SingleEmailMessage mail = 
              new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {a.createdBy.email};
        mail.setToAddresses(toAddresses);
        mail.setReplyTo('noreply@salesforce.com');
        mail.setSenderDisplayName('Batch Job Summary');
        mail.setSubject('Batch job completed');
        mail.setPlainTextBody(emailMessage);
        mail.setHtmlBody(emailMessage);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] 
                           { mail });
    }

    public static testmethod void testBatchAccountOwnerReassignment(){
     // Access the standard user profile  
    
        Profile p = [SELECT Id FROM profile 
                     WHERE name='Standard User']; 

    // Create the two users for the test  
    
        User fromUser = new User(alias = 'newUser1', 
            email='newuser1@testorg.com', 
            emailencodingkey='UTF-8', lastname='Testing', 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey='America/Los_Angeles', 
            username='newuser1@testorg.com');
        User toUser = new User(alias = 'newUser2', 
            email='newuser2@testorg.com', 
            emailencodingkey='UTF-8', lastname='Testing', 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey='America/Los_Angeles', 
            username='newuser2@testorg.com');
        insert fromUser;
        insert toUser;
        
        // Use the new users to create a new account  
    
        List<Account> accs = new List<Account>();
        for(integer i = 0; i < 200; i++){
            accs.add(new Account(name = 'test', 
                     ownerId = fromUser.id));
        }
        insert accs;

        // Actually start the test  
    
        Test.startTest();
        Database.executeBatch(new 
                 AccountOwnerReassignment(fromUser, 
                              toUser));
        Test.stopTest();

        // Verify the test worked  
    
        accs = [SELECT id, name FROM account 
                WHERE ownerId = :toUser.id];
        System.assert(accs.size() == 200);
    }
}

Discussion

The Database.Batchable interface has three methods that must be implemented by your class: start, execute, and finish. This class also includes the method testBatchAccountOwnerReassignment used for testing the class. As a best practice you should always test your code, so this example includes testing.

The following steps through each part of the class.

  1. Implement the interface Database.Batchable.

    Each execution of a batch Apex job is considered a discrete transaction. For example, a batch Apex job that contains 1,000 records and is executed without the optional scope parameter is considered five transactions of 200 records each. By including Database.Stateful in the class declaration, you maintain state across each of these transactions.

    global class AccountOwnerReassignment implements 
                 Database.Batchable<SObject>, 
                 Database.Stateful{
    
  2. Declare the variables used in the rest of the class, as well as the class constructor.
        User fromUser{get; set;}
        User toUser{get; set;}
        Double failedUpdates{get; set;}
     
        global AccountOwnerReassignment(User fromUser, 
                                        User toUser){
            this.fromUser = fromUser;
            this.toUser = toUser;
            failedUpdates = 0;
        }
    
  3. The class implementing Database.Batchable must implement the start method. Use the start method to collect the records or objects to be passed to the interface method execute. This start method populates the QueryLocator object with all the accounts owned by the specified owner.
    global Database.queryLocator 
              start(Database.BatchableContext ctx){
       return Database.getQueryLocator([SELECT id, 
                       name, ownerId 
                       FROM Account 
                       WHERE ownerId = :fromUser.id]);
    }
    
  4. This class must also implement the execute method. The execute method is called for each batch of records passed to the method. Use this method to do all required processing for each chunk of data. This execute method reassigns the owner.
    global void execute(Database.BatchableContext ctx, 
                        List<Sobject> scope){
        List<Account> accs = (List<Account>)scope;
    
        for(Integer i = 0; i < accs.size(); i++){
            accs[i].ownerId = toUser.id;
        }
    
        List<Database.SaveResult> dsrs = 
             Database.update(accs, false);
        for(Database.SaveResult dsr : dsrs){
            if(!dsr.isSuccess()){
                failedUpdates++;
            }
    
        }
    }
    
  5. This class must also implement the finish method. The finish method is called after all batches are processed. This method sends confirmation emails.
        global void finish(Database.BatchableContext 
                           ctx){
    
            AsyncApexJob a = [SELECT id, ApexClassId, 
                          JobItemsProcessed, 
                          TotalJobItems, 
                          NumberOfErrors, 
                          CreatedBy.Email 
                          FROM AsyncApexJob 
                          WHERE id = :ctx.getJobId()];
            
            String emailMessage = 
              'Your batch job '
            + 'AccountOwnerReassignment' 
            + ' has finished.  It executed ' 
            + a.totalJobItems +
            ' batches.  Of which, ' 
            + a.jobitemsprocessed 
            + ' processed without any exceptions' 
            + 'thrown and ' 
            + a.numberOfErrors +
            ' batches threw unhandled exceptions.'
            + '  Of the batches that executed' 
            + 'without error, ' 
            + failedUpdates 
            + ' records were not updated successfully.';
            
         Messaging.SingleEmailMessage mail = 
                  new Messaging.SingleEmailMessage();
         String[] toAddresses = new String[] 
                  {a.createdBy.email};
         mail.setToAddresses(toAddresses);
         mail.setReplyTo('noreply@salesforce.com');
         mail.setSenderDisplayName('Batch Job Summary');
         mail.setSubject('Batch job completed');
         mail.setPlainTextBody(emailMessage);
         mail.setHtmlBody(emailMessage);
         Messaging.sendEmail(new 
              Messaging.SingleEmailMessage[] { mail });
        }                                
    
  6. Generate the setup data for testing your code. Create the two users that are necessary for the test, as well as the 200 accounts:
    public static testmethod void 
                    testBatchAccountOwnerReassignment(){
        // Access the standard user profile
        Profile p = [SELECT Id FROM profile 
                     WHERE name='Standard User']; 
    
        // Create the two users for the test
        User fromUser = new User(alias = 'newUser1', 
            email='newuser1@testorg.com', 
            emailencodingkey='UTF-8', 
            lastname='Testing', 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey='America/Los_Angeles', 
            username='newuser1@testorg.com');
        User toUser = new User(alias = 'newUser2', 
            email='newuser2@testorg.com', 
            emailencodingkey='UTF-8', 
            lastname='Testing', 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey='America/Los_Angeles', 
            username='newuser2@testorg.com');
        insert fromUser;
        insert toUser;
    
        // Use the new users to create a new account
        List<Account> accs = new List<Account>();
        for(integer i = 0; i < 200; i++){
            accs.add(new Account(name = 'test', 
            ownerId = fromUser.id));
        }
        insert accs; 
    
  7. The executeBatch method starts an asynchronous process. However, your batch job must finish before you can test against the results. Use the Test methods startTest and stopTest around the executeBatch method to ensure the asynchronous process finishes before you test. When startTest is called all asynchronous calls get accumulated by the system. When the stopTest method is called, all asynchronous jobs are run synchronously.
    Asynchronous calls, such as @future or executeBatch, called in a startTest,*stopTest* block, do not count against your organization limits for the number of queued jobs.
        // Actually start the test
        Test.startTest();
        Database.executeBatch(new 
            AccountOwnerReassignment(fromUser, toUser));
        Test.stopTest();
    
        // Verify the test worked
        accs = [SELECT id, name FROM account 
                WHERE ownerId = :toUser.id];
        System.assert(accs.size() == 200);
    }                       
    

Share

Recipe Activity - Please Log in to write a comment

"You can only have five queued or active batch jobs at one time. Use extreme care if you are planning to invoke a batch job from a trigger. You must be able to guarantee that the trigger will not add more batch jobs than the five that are allowed. In particular, consider API bulk updates, import wizards, mass record changes through the user interface, and all cases where more than one record can be updated at a time."

Could you provide an example how a batch could be safely called from a trigger?

by Eric_Santiago  (2010-12-15)

X

Vote to Verify a Recipe

Verifying a recipe is a way to give feedback to others and broaden your own understanding of the capabilities on Force.com. When you verify a recipe, please make sure the code runs, and the functionality solves the articulated problem as expected.

Please make sure:
  • All the necessary pieces are mentioned
  • You have tested the recipe in practice
  • Have sent any suggestions for improvements to the author

Please Log in to verify a recipe

You have voted to verify this recipe.