2

hoping I can get some help with Batch Apex as I have zero experience with it and the docs aren't all that good.

I have a class that does the following: 1. Constructs a Set of Account Ids 2. Constructs a Set of Contract Ids, where the Contract is related to an Account found in step 1. 3. Constructs a Map of 4. Constructs a List of Attachments, updating ParentID from the Map in step 3. (relocating the attachment from the Contract to the root Account) 5. Updates the Attachments to now sit at the Account level.

This is working fine on small amounts of data - but as soon as I test in a full data sandbox it hits the governor limits.

Does anyone know how I could write this class in Batch Apex? I really have no idea.

Thanks.

global class AttachmentReparentPDFsToAccount {
    webservice static string reparentPDFs () {

    // Set containing the Ids of the Accounts that will be migrated
        Set<Id> accountIdSet = new Set<Id> {};
        for (Account acc:[SELECT Id FROM Account WHERE (Type = 'Subscriber' OR Type = 'Ex-Subscriber' OR Type = 'Division' OR Type = 'EDU SUBSCRIBER')]){
            accountIdSet.add(acc.Id);
            System.debug('No. of valid accounts: ' + accountIdSet.size());
        }

        // Set containing the Ids of the Contracts that will be migrated
        Set<Id> contractIdSet = new Set<Id> {};
        for (Contract con:[SELECT Id FROM Contract WHERE AccountId IN :accountIdSet]){
                contractIdSet.add(con.Id);
                System.debug('No. of valid contracts: ' + contractIdSet.size());
        }

        // Create the map that will contain ContractId => Contract.AccountId
        Map<Id,Id> contractToAccountMap=new Map<Id,Id>();

        // Lists of attachments to be created then deleted
        List<Attachment> attachmentsToDelete = new List<Attachment>(); 
        List<Attachment> attachmentsToCreate = new List<Attachment>(); 

        // Populate the map with the ContractId and AccountId related to the Contract
        for(Contract ctr:[Select Id,AccountId from Contract where Id in:contractIdSet]){
            contractToAccountMap.put(ctr.Id,Ctr.AccountId);
        }

        // Attachment tempAtt = new Attachment();
        // Update the parentId of the attachment, changing from the ContractId to the related AccountId
        for(Attachment att:[SELECT Name, Id, Body, ParentId From Attachment WHERE ((Name LIKE '%.pdf') AND (ParentId IN :contractIdSet))]){
            if(contractToAccountMap.get(att.ParentID)!=null){
                Attachment tempAtt = new Attachment ( name = att.name, body = att.body, parentId = contractToAccountMap.get(att.ParentID));
                // tempAtt.ParentID = contractToAccountMap.get(att.ParentID);
                    attachmentsToCreate.add(tempAtt);
                    attachmentsToDelete.add(att);
            }
        }

        // Update the list of attachments
        insert attachmentsToCreate;
        delete attachmentsToDelete;
        System.debug('No. of attachments to be created; ' + attachmentsToCreate.size());

        // Return the no. of attachments that have been reparented
        String listSize =  String.valueOf(attachmentsToCreate.size());
        listSize += ' attachments have been reparented to their root Accounts.';
        return(listSize);

    }
}
  • 1
    Governor limits generally apply in batch apex as well, script statements, etc. Maybe you can post some of what you have so far an someone can convert it to batch more easily without having to write it from scratch. – jordan.baucke Oct 17 '12 at 15:39
2

I think you can also simplify your code somewhat by adding a sub-select to the SOQL in the start method. You will also need to implement Database.Stateful to pass state between execute and finish methods should you wish to report back to the user (yourself?).

Read up on Batch Apex here http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_batch_interface.htm

global class BatchAttachmentReparentPDFsToAccount implements Database.Batchable<sObject>, Database.Stateful
{
    private String listSize;

    global Database.QueryLocator start( Database.BatchableContext BC ) 
    {
        return Database.getQueryLocator( [SELECT Id,(Select Id From Contracts)  FROM Account WHERE (Type = 'Subscriber' OR Type = 'Ex-Subscriber' OR Type = 'Division' OR Type = 'EDU SUBSCRIBER')] );
    }

    global void execute( Database.BatchableContext BC, List<sObject> scope )
    {
        Set<Id> accountIdSet = new Set<Id> {};
        Map<Id,Id> contractToAccountMap=new Map<Id,Id>();

        for( Account acc : (List<Account>)scope )
        {
            accountIdSet.add( acc.Id );      

            for( Contract ct : acc.Contracts )
            {
                if( !contractToAccountMap.containsKey( ct.Id ) )
                {
                  contractToAccountMap.put( ct.Id, acc.Id );
                }
            }
        }

        // Lists of attachments to be created then deleted
        List<Attachment> attachmentsToDelete = new List<Attachment>(); 
        List<Attachment> attachmentsToCreate = new List<Attachment>(); 


        // Attachment tempAtt = new Attachment();
        // Update the parentId of the attachment, changing from the ContractId to the related AccountId
        List<Attachment> attachments = [SELECT Name, Id, Body, ParentId From Attachment WHERE ((Name LIKE '%.pdf') AND (ParentId IN :contractToAccountMap.keySet() ))];
        for(Attachment att: attachments ){
            if(contractToAccountMap.get(att.ParentID)!=null){
                Attachment tempAtt = new Attachment ( name = att.name, body = att.body, parentId = contractToAccountMap.get(att.ParentID));
                // tempAtt.ParentID = contractToAccountMap.get(att.ParentID);
                    attachmentsToCreate.add(tempAtt);
                    attachmentsToDelete.add(att);
            }
        }

        // Update the list of attachments
        insert attachmentsToCreate;
        delete attachmentsToDelete;
        System.debug('No. of attachments to be created; ' + attachmentsToCreate.size());

        // Return the no. of attachments that have been reparented
        listSize =  String.valueOf(attachmentsToCreate.size());
        listSize += ' attachments have been reparented to their root Accounts.';

    }

    global void finish( Database.BatchableContext BC )
    {
        // send an email with listSize?
    }

}
|improve this answer|||||
  • 1
    I would recommend keeping the actual SOQL statement outside the batch, and pass it in as a String - that way you have flexibility for using different queries in the future. I would also be wary of using that type of sub-select with the query locator in batches - you'll see several posts on the discussion boards about how those sub queries seem to break in a batch context. So I agree the sub-select would work well here - but I would perform that query in the batch itself just for the particular scope being processed... – BritishBoyinDC Oct 17 '12 at 17:14
  • Looks great and deployed to my Sandbox just fine - now how do I run it? Sorry for the stupid question! – Davin Casey Oct 17 '12 at 17:37
  • Good to know about the sub-select @BritishBoyinDC. You can start this from your developer console BatchAttachmentReparentPDFsToAccount batch = new BatchAttachmentReparentPDFsToAccount (); Database.executeBatch( batch, 1 ); See the other answer for batch sizes (this one is 1, i.e it will process 1 Account at a time) – Phil Hawthorn Oct 17 '12 at 17:50
2

So if you already have a class that is working, you can just call it from a batch process, and keep the scope (i.e. the number of records each batch execution processes) low, and you shoudl be fine with governor limits...so you would just need to modify your class to receive the accounts rather than query for them in the class

So for example, if I had this class that receives a list of accounts:

public class ContractProcessing {
    public Boolean process (List<Account> accountsforprocessing ) {
        for (Account a: accountsforprocessing) {
            a.Type = 'Technology Partner';
        }

        try {
            update accountsforprocessing;
            return true;
        } catch (Exception Ex) {
            //do exception process
            return false;
        }
    } //end method    
}

I could call it from a batch class like this:

global class BatchContractProcessing implements Database.Batchable<sObject> {

    public string query;

    //Execute the query.
    global database.querylocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope) {
        List<Account> accts = (List<Account>)scope;
        ContractProcessing cp = new ContractProcessing ();
        Boolean result = cp.process(accts);
    }

    global void finish(Database.BatchableContext BC) {
    }
}

To set the list of accounts, I would invoke the batch via the dev console or scheduler, or a button if you like - the code looks like this:

BatchContractProcessing bcp = new BatchContractProcessing ();
bcp.query = 'Select Id, Name, Type from Account WHERE NAME LIKE \'Test%\' ';
database.executebatch(bcp,1);

The first line creates the class, the second line creates the query of all accounts I want to be processed by this batch, and third line invokes the batch - notice the number 1 as a parameter - that means each execution of the batch only processes one account - and it is each execution of the batch that is restricted by governor limits - so as long as the code can handle updating one account, we are fine. You can increase the batch size - the default is 200 if your code can handle that volume...since the lower the scope, the longer the batch takes...

|improve this answer|||||
1

Thanks everyone for your help!

Putting both classes here in case anyone has a similar question in future.

I had to get a little creative when writing the test class.

// BatchAttachmentReparentPDFsToAccount
//
// Class designed to find all .PDF files attached to the Contract object, where the Account is to be migrated as per the Project Enable migration and:
// 1. Create a copy of the .PDF file & relate it to the root Account
// 2. Delete the original .PDF file from the Contract

global class BatchAttachmentReparentPDFsToAccount implements Database.Batchable<sObject>, Database.Stateful
{
  Set<String> criteria = new Set<String>{'Subscriber', 'Ex-Subscriber', 'Division', 'EDU Subscriber', 'Live Lead', 'Lead'};

  // Define the initial SOQL query
  public String query = 'SELECT Id, (Select Id From Contracts) FROM Account WHERE Type IN :criteria';

   // Query the relevant Accounts & related Contracts
      global Database.QueryLocator start( Database.BatchableContext BC )  {
          return Database.getQueryLocator(query);
      } // end of start method

  // Find & insert/delete the attachments where needed
      global void execute( Database.BatchableContext BC, List<sObject> scope ) {

        // Create the Set & Map that will hold the Account & Contract IDs
          Set<Id> accountIdSet = new Set<Id> {};
          Map<Id,Id> contractToAccountMap=new Map<Id,Id>();

    // Populate contractToAccountMap with ContractId & AccountId values
          for( Account acc : (List<Account>)scope ) {
                accountIdSet.add( acc.Id );      
      for( Contract ct : acc.Contracts ) {
                    if( !contractToAccountMap.containsKey( ct.Id ) ) {
                        contractToAccountMap.put( ct.Id, acc.Id );
                    }
                }
          }

          // Lists of attachments to be created then deleted
          List<Attachment> attachmentsToDelete = new List<Attachment>(); 
          List<Attachment> attachmentsToCreate = new List<Attachment>(); 


          // Update the parentId of the attachment, changing from the ContractId to the related AccountId
          List<Attachment> attachments = [SELECT Name, Id, Body, ParentId From Attachment WHERE ((Name LIKE '%.pdf') AND (ParentId IN :contractToAccountMap.keySet() ))];
          for(Attachment att: attachments ) {
                if(contractToAccountMap.get(att.ParentID)!=null) {
                    Attachment tempAtt = new Attachment ( name = att.name, body = att.body, parentId = contractToAccountMap.get(att.ParentID));
                    // tempAtt.ParentID = contractToAccountMap.get(att.ParentID);
                    attachmentsToCreate.add(tempAtt);
                    attachmentsToDelete.add(att);
                }
          }

    // delete the Contract attachments
          delete attachmentsToDelete;

          // create the Account attachments
          insert attachmentsToCreate;

  } // end of execute method

  // Send an email with job details
      global void finish( Database.BatchableContext BC ) {

          AsyncApexJob a = [  SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email
                    FROM AsyncApexJob 
                    WHERE Id = :BC.getJobId()
    ];

     // Send an email to the Apex job's submitter notifying of job completion. 
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    String[] toAddresses = new String[] {a.CreatedBy.Email};
    mail.setToAddresses(toAddresses);
    mail.setSubject('Apex Sharing Recalculation ' + a.Status);
    mail.setPlainTextBody
    ('The batch Apex job processed ' + a.TotalJobItems +
    ' batches with '+ a.NumberOfErrors + ' failures.');
    Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
      } // end of finish method
}

and the test class:

@isTest
private class BatchAttachmentReparentPDFsToAccountTest {

      static testMethod void BatchAttachmentReparentPDFsToAccountTest() {

          Test.startTest();

          // Create the test records
          Account Acc = new Account(name = 'Test Account 1');
          insert Acc;
          Contract Ctr = new Contract(name = 'Test Contract 1', AccountId = Acc.Id);
          insert Ctr;
          Attachment Att = new Attachment(name = 'Test Attachment 1', ParentId = Ctr.Id, body = Blob.valueOf('Unit Test Attachment Body'));
          insert Att;

          Set<String> criteria = new Set<String>{'Subscriber', 'Ex-Subscriber', 'Division', 'EDU Subscriber', 'Live Lead', 'Lead'};
          BatchAttachmentReparentPDFsToAccount batch = new BatchAttachmentReparentPDFsToAccount (); 
          batch.query = 'SELECT Id, (Select Id From Contracts) FROM Account WHERE Type IN :criteria LIMIT 200';
      Database.executeBatch( batch, 1 );
          Test.stopTest();

      }
}
|improve this answer|||||
  • 2
    Unaccepting an answer in order to accept your own based off of it, and accept that is probably considered bad etiquette. It's generally better to propose an edit to the good answer with your new code. – ca_peterson Oct 23 '12 at 16:38
  • 1
    My apologies - I didn't realise marking my last post as an answer would remove the answer flag from the original answer. Definitely not intended! If @philhawthorn wants to include my addition to his answer he's more than welcome to, I will then delete this answer. – Davin Casey Oct 24 '12 at 9:35
  • 1
    Thanks guys, leave it with me and I'll get to it when I get a minute :) – Phil Hawthorn Oct 24 '12 at 10:25

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.