1

I have this batch apex class where in the START method I am pulling all the workOrders in the Org. As of now what we have in the org is 30000 records and will keep increasing in the future. For each work order there is a vendor field which is a lookup to the account record and it also indicates to what vendor that work order is assigned to.

Here is my START method -

global Database.QueryLocator start(Database.BatchableContext BC)
{
    String query = 'SELECT Id, Vendor__c, Purchase_Order_Created_Date__c, Date_Repair_Completed__c FROM WorkOrder WHERE Status = \'Billing Finalized\'';
       
    return Database.getQueryLocator(query);
}

In the execute method, I’m grouping work orders by vendor account and updating specific fields on each account based on related work order data. Given that execute processes only 200 records at a time, will the map include all work orders for each vendor to ensure accurate updates? If not, how can I ensure the map contains all the work order records per vendor account before proceeding?

Here is my EXECUTE method -

global void execute(Database.BatchableContext BC, List<WorkOrder> scope)
{
    Map<Id, List<WorkOrder>> vendorWorkOrders = new Map<Id, List<WorkOrder>>();
    for (WorkOrder wo : scope) {
       if (wo.Vendor__c != null) {
         if (!vendorWorkOrders.containsKey(wo.Vendor__c)) {
             vendorWorkOrders.put(wo.Vendor__c, new List<WorkOrder>());
         }
         vendorWorkOrders.get(wo.Vendor__c).add(wo);
       }
    }

    for (Id vendorId : vendorWorkOrders.keySet()) {
        List<WorkOrder> workOrdersForVendor = vendorWorkOrders.get(vendorId);
        // Processing logic here for each vendor's work orders
    }
}

Please advise!

1 Answer 1

3

Nope, it is not guaranteed that all instances of a particular Vendor__c value will be constrained to a single call to the execute method (i.e. a single batch, a single scope). The easy example here is if a single Vendor has, say, 300 Work Orders (with the default batch size of 200) that match your criteria.

Even if a given Vendor only has 2 appropriate Work Orders, there's no guarantee that those Work Orders would be in the same batch. One could be in the first batch, the second could be in the final batch, even if you include ORDER BY Vendor__c.

You need to shift mentality here a bit, and base this batch class off of Account instead.

  • Return a query locator for your Vendor Accounts (I'm assuming that's either a RecordType, or you have some field that can be used to distinguish Vendor and Non-Vendor Accounts). Just return the Account Id, don't try to query for Work Orders yet (subqueries in the data returned by the start() method degrades performance)
  • In your execute() method, run your query for the target Work Orders

Since the snippet you've provided shows organizing WorkOrders by Account, using a subquery in the execute() method would make sense. Let SOQL do the work for you instead of writing it yourself in Apex.

List<Account> vendorsWithWorkOrders = [
    SELECT 
        Id, 
        (SELECT Id, Purchase_Order_Created_Date__c, Date_Repair_Completed__c FROM WorkOrders__r WHERE Status = 'Billing Finalized')
    FROM Account
    WHERE Id IN :scope];

Using a subquery like that (instead of just directly querying Work Orders) also increases the number of Work Orders you can get back. I think there's some documentation somewhere that says the limit for subquery rows is 3x the normal limit (so 150,000 rows in subqueries per transaction), but I could swear that limit isn't checked/enforced. At least not in my org when I was testing something similar.

Just be sure to use a nested loop to iterate over all of the Work Orders when you do the rest of your processing instead of trying to access the child list directly

for(Account vendor : vendorsWithWorkOrders) {
    // This will frequently result in an exception unless you can guarantee that
    //   no Vendor will have over 200 qualifying Work Orders at once
    //List<WorkOrder> workOrders = vendor.WorkOrders__r;

    // The safe way is to just use a nested loop
    List<WorkOrder> workOrders = new List<WorkOrder>();
    for(WorkOrder wo : vendor.WorkOrders__r) {
        workOrders.add(wo);
    }

    // This is how you skip over Vendors that don't have any WorkOrders to process
    // "continue;" skips the rest of this iteration of the loop, and gets you
    //   to the next iteration
    if(workOrders.isEmpty()) {
        continue;
    }

    // The rest of your processing, as normal (probably)
}

Not all nested loops are evil. It's mostly the ones of the form

// This is an example of an evil nested loop, that gets you into O(N^2) territory 
// If you find yourself doing something like this, loop over list2 first
//   and build a map instead.
for(Integer i = 0; i < list1.size(); i++) {
    for(Integer j = 0; j < list2.size(); j++) {
        if(list1[i].SomeField__c == list2[j].Another_Field__c) {
            // do work
        }
    }
}

that should be avoided. The above nested loop would run a total of list1.size() * list2.size() times, whereas a nested loop over the results of a subquery would only run a total of list2.size() times

2
  • The subquery in the execute() method approach should also allow you to increase your batch size. Fewer batches = less overhead for Salesforce = processing finishes sooner
    – Derek F
    Commented 23 hours ago
  • Thanks Derek, I really appreciate the level of detail in the answer provided.
    – sumchans
    Commented 21 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.