深度剖析Salesforce中的SOQL query for loop功能

890 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

在看Working with Very Large SOQL Queries这一章节的文档时,尽管例子很详细,但对于该技术背后的逻辑任然一知半解,因此,本章将在在官方文档例子的基础上结合调研的实践经验,来厘清该技术背后的逻辑。希望能探索出合适的应用场景,来满足日常开发中的真实需要。

官方介绍

#1 应用价值:避免因执行soql返回大量数据,导致数据处理的过程heap size超限以及其它隐形问题。

Your SOQL query sometimes returns so many sObjects that the limit on heap size is exceeded and an error occurs. To resolve, use a SOQL query for loop instead, since it can process multiple batches of records by using internal calls to query and queryMore.

#2 语法格式:

// Use this format if you are not executing DML statements 
// within the for loop
for (Account a : [SELECT Id, Name FROM Account 
                  WHERE Name LIKE 'Acme%']) {
    // Your code without DML statements here
}
 
// Use this format for efficiency if you are executing DML statements 
// within the for loop
for (List<Account> accts : [SELECT Id, Name FROM Account
                            WHERE Name LIKE 'Acme%']) {
    // Your code here
    update accts;
}

#3 示例代码:mass update records

public void massUpdate() {
    for (List<Contact> contacts: [SELECT FirstName, LastName 
                                  FROM Contact]) {
        for(Contact c : contacts) {
            if (c.FirstName == 'Barbara' && c.LastName == 'Gordon') {
                c.LastName = 'Wayne';
            }
        }
        update contacts;
    }
}

实验1

实验目的:结合#3,一个事务中最容易触发两类限制:
a. SOQL 50000限制;
b. DML 10000限制;
那么使用soql query for loop是否有助于消除这些限制顾虑。

实验介绍:现有自定义对象Test__c,字段包含Name, No__c(AutoNo)和Picklist__c(A, B, C),目前系统中有A, B, C类记录数分别为3w(共9w),其Name分别为1,2,3,初始化数据如下:

​编辑

实验代码:以Picklist__c + '-' + No__c格式跟新9w条Test记录的Name值。i.e. A-00001

public class TestHelper {
    // soql query for loop for large data query and mass update
    public static void massUpdateTest() {
        System.debug(LoggingLevel.INFO, '*** [start time]: ' + System.now().format('yyyy-MM-dd HH:mm:ss'));
        Integer i = 0;
        // [error01] - System.LimitException: Too many DML rows: 10001
        for(List<Test__c> tList : [SELECT Id, Name, No__c, Picklist__c 
                                   FROM Test__c LIMIT 50000]) {// fix [error01-01] final i is 250
            i++;
            Integer j = 0;
            for(Test__c t : tList) {
                j++;
                t.Name = t.Picklist__c + '-' + t.No__c;
            }
            // [error02] - System.LimitException: Too many DML rows: 10001
            if(i*200 <= 10000) update tList;// fix [error02]
            System.debug(LoggingLevel.INFO, '*** count[i]: ' + i);
            // System.debug(LoggingLevel.INFO, '*** count[j]: ' + j);
            // if(i*200 >= (50000/200-1)*200) break;// fix [error01-02] final i is 249
        }
        System.debug(LoggingLevel.INFO, '*** [end time]: ' + System.now().format('yyyy-MM-dd HH:mm:ss'));
    }
}

分析与结论:上述代码对应行号如下: 在使用TestHelper.massUpdateTest();批量更新记录时,我们遇到了两类问题:
#1. 无法按预期更新Test记录Name值,见注释line15 [error02] - System.LimitException: Too many DML rows: 10001,因此b中顾虑仍在;
#2. 在解决[error02]后,依然不能打印[end time]值,原因是9w数据超出了soql 5w限制,即a中顾虑仍在。此时可使用line8和line19能避免runtime层面报错;

值得一提的是,soql for loop机制中,父子循环中父循环每个批次处理200条记录。另外如果在子循环使用了continue,跳过当前记录后处理的不再是下一条记录,而是下一个批次,即对应父循环跳过当前批次,进入下一轮记录数为200的批次。

The break and continue keywords can be used in both types of inline query for loop formats. When using the sObject list format, continue skips to the next list of sObjects. refer to: SOQL For Loops

最后,回到【官方介绍】#1 应用价值,其实质是在解决heap size的问题,结合line19 final i is 249和line8 final i is 250,对应的log信息如下:

​编辑

​编辑

实验2

实验目的:比较soql foor loop与std soql query在性能上的差异,如soql for loop对heap size的贡献。 实验代码:

public class TestHelper {
    // std soql query
    public static void massUpdateTest1() {
        System.debug(LoggingLevel.INFO, '*** [start time]: ' + System.now().format('yyyy-MM-dd HH:mm:ss'));
        Integer i = 0;

        List<Test__c> uList = new List<Test__c>(),
                      tList = [SELECT Id, Name, No__c, Picklist__c
                               FROM Test__c LIMIT 50000];

        for(Test__c t : tList) {
            i++;
            t.Name = t.Picklist__c + '-' + t.No__c;
            if(i <= 10000) uList.add(t);
        }

        if(uList.size() > 0) update uList;
        System.debug(LoggingLevel.INFO, '*** [end time]: ' + System.now().format('yyyy-MM-dd HH:mm:ss'));
    }
}

分析与结论: ​编辑

我们通过对比实验1与实验2的执行log,发现实验2并未能验证出soql for loop对heap size的贡献,但发现soql for loop更消耗CPU,因为每个批次都对应update操作,而update本身相比查询而言耗费更大。

Heap Size】:Error 'Apex heap size too large'Build better apex scripts to manage heap limits | What To Do When Salesforce Apex Heap Size Increases?What is Heap size?

@isTest
private class heapCheckIssue{
    static testMethod void badCodeTest() {
        String tStr = 'aaaaa bbbbb ccccc ddddd eeeeee fffff ggggg 11111 22222 33333 44444';
        List<String> baseList = tStr.split(' ');
        List<String> bigList = baseList;
        Map<integer, List<String>> SampleMap = new Map<integer, List<String>>();
        SampleMap.put(1, bigList);

        for (integer i=0; i<50; i++) {
            List<String> tempList = new List<String>();
            tempList = SampleMap.get(1);
            bigList.addAll(tempList);
        }
        system.debug('FINAL LIST SIZE IS '+bigList.size());
    }

    static testMethod void goodCodeTest() {
        String tStr = 'aaaaa bbbbb ccccc ddddd eeeeee fffff ggggg 11111 22222 33333 44444';
        List<String> baseList = tStr.split(' ');
        Map<integer, List<String>> Sample = new Map<integer, List<String>>();
        List<String> bigList = baseList;

        Sample.put(1, bigList);
        List<string> myList = new list<string>(); //Declare a new list

        for (integer i=0; i<50; i++) {
            List<String> tempList = new List<String>();
            tempList = Sample.get(1);
            system.debug('templist: ' + tempList.size());
            system.debug(' bigList: ' + bigList.size());

            myList.addall(tempList); //original code is  bigList.addall(tempList);
        }

        system.debug('FINAL LIST SIZE OF bigList IS '+ bigList.size());
        system.debug('myList IS '+mylist.size());
    }
}