Sunday 19 May 2024

Use Cursors for Expanded SOQL Query Result Support (Beta) - Summer ’24 Release

With Apex cursors, you can break up the processing of a SOQL query result into pieces that can be processed within the bounds of a single transaction. Cursors provide you with the ability to work with large query result sets, while not actually returning the entire result set. You can traverse a query result in parts, with the flexibility to navigate forward and back in the result set. Package developers and advanced developers can use cursors effectively to work with high-volume and high-resource processing jobs. Cursors are an alternative to batch Apex and address some of batch Apex’s limitations. Cursors are also more powerful because they can be used in a chain of queueable Apex jobs.

 

How: Apex cursors are stateless and generate results from the offset that is specified in the Cursor.fetch(integer position, integer count) method. You must track the offsets or positions of the results within your particular processing scenario.

A cursor is created when a SOQL query is executed on a Database.getCursor() or Database.getCursorWithBinds() call. When a Cursor.fetch(integer position, integer count) method is invoked with an offset position and the count of records to fetch, the corresponding rows are returned from the cursor. The maximum number of rows per cursor is 50 million, regardless of the operation being synchronous or asynchronous. To get the number of cursor rows returned from the SOQL query, use Cursor.getNumRecords().

Apex cursors throw these new System exceptions: System.FatalCursorException and System.TransientCursorException. Transactions that fail with System.TransientCursorException can be retried.


  
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class QueryChunkingQueuable implements Queueable {
    private Database.Cursor locator;
    private integer position;
 
    public QueryChunkingQueuable() {
        locator = Database.getCursor
                  ('SELECT Id FROM Contact WHERE LastActivityDate = LAST_N_DAYS:400');
        position = 0;
    }
 
    public void execute(QueueableContext ctx) {
        List<Contact> scope = locator.fetch(position, 200);
        position += scope.size();
        // do something, like archive or delete the scope list records
        if(position < locator.getNumRecords() ) {
            // process the next chunk
            System.enqueueJob(this);
        }
    }
}
 

  

Apex cursors have the same expiration limits as API Query cursors.

To get Apex cursor limits, use these new methods in the Limits class.

  • Limits.getApexCursorRows() and its upper bound Limits.getLimitApexCursorRows() method

  • Limits.getFetchCallsOnApexCursor() and its upper bound Limits.getLimitFetchCallsOnApexCursor() method

These Apex governor limits have been updated with this feature.

  • Maximum number of rows per cursor: 50 million (both synchronous and asynchronous)

  • Maximum number of fetch calls per transaction: 10 (both synchronous and asynchronous) 

  • Maximum number of cursors per day: 10,000 (both synchronous and asynchronous)

  • Maximum number of rows per day (aggregate limit): 100 million

Dynamically Pass Bind Variables to a SOQL Query

With the new Database.queryWithBinds, Database.getQueryLocatorWithBinds, and Database.countQueryWithBinds methods, the bind variables in the query are resolved from a Map parameter directly with a key rather than from Apex code variables.

Where: This change applies to Lightning Experience and Salesforce Classic in Enterprise, Performance, Unlimited, and Developer editions.

How: In this example, the SOQL query uses a bind variable for an Account name. Its value (Acme Inc.) is passed in to the method using the nameBind Map. The accountName variable isn't (and doesn’t have to be) in scope when the query is executed within the method.


  
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public static List<Account> simpleBindingSoqlQuery(Map<String, Object> bindParams) {
    String queryString =
        'SELECT Id, Name ' +
        'FROM Account ' +
        'WHERE name = :name';
    return Database.queryWithBinds(
        queryString,
        bindParams,
        AccessLevel.USER_MODE
    );
}
 
String accountName = 'Acme Inc.';
Map<String, Object> nameBind = new Map<String, Object>{'name' => accountName};
List<Account> accounts = simpleBindingSoqlQuery(nameBind);
System.debug(accounts);