Apex Cursors Spring 26 Processando 50 Mi de Registros GuiaApex Cursors Spring 26 Processando 50 Mi de Registros Guia

Por que o Apex Cursor muda o jogo para quem trabalha com grandes volumes de dados

Se você já precisou processar milhões de registros no Salesforce, sabe como a situação pode se complicar rápido. O Batch Apex resolve o problema, mas cobra seu preço: overhead de execução, controle limitado sobre o ciclo de processamento e uma rigidez que nem sempre combina com cenários reais de alta demanda. Pensando nessa dor, a Salesforce introduziu no Spring ’26 (API v66.0) duas classes que vão transformar a forma como desenvolvedores lidam com grandes conjuntos de dados: Cursor e PaginationCursor.

A ideia central é simples em conceito e poderosa na prática: em vez de carregar todos os registros de uma query na memória de uma vez, o Apex Cursor cria um ponteiro para o resultado da consulta. Você decide quantos registros quer buscar por vez, de onde começa e quando avança ou retrocede. É como ter um controle remoto para navegar em um resultado de SOQL que pode ter até 50 milhões de linhas, sem nunca estourar os limites de heap ou de coleções do Apex.

Como funciona o Database.getCursor()

O ponto de partida é invocar o método Database.getCursor() passando uma query SOQL. O Salesforce retorna um objeto do tipo Database.Cursor que mantém referência ao resultado completo, mas não carrega nada na memória até você chamar explicitamente o método fetch().

// Criando um cursor para uma query com resultado potencialmente grande
Database.Cursor cursor = Database.getCursor(
    'SELECT Id, Name, Industry, AnnualRevenue FROM Account ORDER BY Name'
);

// Descobrindo o total de registros disponíveis
Integer totalRecords = cursor.getNumRecords();
System.debug('Total de registros no cursor: ' + totalRecords);

// Buscando os primeiros 200 registros
List<Account> batch1 = cursor.fetch(0, 200);
System.debug('Primeiro lote: ' + batch1.size() + ' registros');

// Buscando o próximo lote a partir da posição 200
List<Account> batch2 = cursor.fetch(200, 200);
System.debug('Segundo lote: ' + batch2.size() + ' registros');

O método fetch(Integer position, Integer count) aceita dois parâmetros: a posição inicial (zero-indexed) e a quantidade de registros que você quer retornar. Cada chamada de fetch consome uma query SOQL e as linhas retornadas contabilizam no governor limit de SOQL rows, então o planejamento do tamanho do lote continua sendo uma decisão de design importante.

Substituindo Batch Apex com Cursor e Queueable encadeado

O verdadeiro poder dos Apex Cursors aparece quando você os combina com Queueable Apex. Em vez de criar uma classe que implementa Database.Batchable com toda a cerimônia de start, execute e finish, você pode construir uma cadeia de jobs Queueable que cada um processa um pedaço do resultado usando o cursor. O controle sobre o tamanho do lote, a velocidade de processamento e o tratamento de erros fica muito mais granular.

public class AccountProcessorQueueable implements Queueable {
    private Database.Cursor cursor;
    private Integer currentPosition;
    private Integer batchSize;

    public AccountProcessorQueueable(
        Database.Cursor cursor, Integer position, Integer batchSize
    ) {
        this.cursor = cursor;
        this.currentPosition = position;
        this.batchSize = batchSize;
    }

    public void execute(QueueableContext context) {
        List<Account> records = cursor.fetch(currentPosition, batchSize);

        if (records.isEmpty()) {
            System.debug('Processamento concluído.');
            return;
        }

        for (Account acc : records) {
            acc.Description = 'Processado via Cursor em ' + Datetime.now();
        }
        update records;

        Integer nextPosition = currentPosition + batchSize;
        if (nextPosition < cursor.getNumRecords()) {
            System.enqueueJob(
                new AccountProcessorQueueable(cursor, nextPosition, batchSize)
            );
        }
    }
}

// Iniciando o processamento
Database.Cursor cursor = Database.getCursor(
    'SELECT Id, Name, Description FROM Account WHERE AnnualRevenue > 1000000'
);
System.enqueueJob(new AccountProcessorQueueable(cursor, 0, 200));

Esse padrão oferece vantagens claras sobre o Batch Apex tradicional. Você tem controle total sobre o tamanho do lote sem precisar implementar o método Database.BatchableContext. Pode alterar dinamicamente o tamanho do lote entre execuções, pausar e retomar o processamento, e até priorizar determinados registros. Além disso, o cursor não exige a serialização de estado entre execuções, o que reduz significativamente o overhead de cada etapa.

PaginationCursor: a solução para paginação na interface

Enquanto o Database.Cursor é ideal para processamento em background, o Database.PaginationCursor foi desenhado para um cenário diferente: paginação de resultados na interface do usuário. A diferença fundamental está na consistência do tamanho das páginas.

Imagine que um usuário está navegando por uma lista de contas e, entre uma página e outra, alguns registros foram deletados por outro processo. Com um cursor normal, a segunda página poderia retornar menos registros que o esperado, criando uma experiência confusa. O PaginationCursor resolve isso automaticamente: quando registros são deletados entre chamadas de fetchPage(), ele compensa automaticamente, garantindo que o tamanho da página permaneça consistente.

// Criando um Pagination Cursor
Database.PaginationCursor pagCursor = Database.getPaginationCursor(
    'SELECT Id, Name, Industry, CreatedDate FROM Account ORDER BY CreatedDate DESC'
);

// Buscando a primeira página de 10 registros
Database.CursorFetchResult page1 = pagCursor.fetchPage(0, 10);
List<Account> accounts = (List<Account>) page1.getRecords();

System.debug('Registros na página 1: ' + accounts.size());
System.debug('Próximo índice: ' + page1.getNextIndex());
System.debug('Paginação concluída? ' + page1.isDone());

// Buscando a próxima página usando o índice retornado
if (!page1.isDone()) {
    Database.CursorFetchResult page2 = pagCursor.fetchPage(page1.getNextIndex(), 10);
    List<Account> accounts2 = (List<Account>) page2.getRecords();
    System.debug('Registros na página 2: ' + accounts2.size());
}

O PaginationCursor trabalha com limites diferentes do Cursor padrão: suporta até 100.000 registros por cursor (contra 50 milhões do Cursor regular), mas permite até 200.000 instâncias por período de 24 horas (contra 10.000 do Cursor). O tamanho máximo de página é de 2.000 registros. Essa troca faz sentido: para paginação de UI, você precisa de muitas instâncias leves, não de um resultado massivo.

Exemplo prático: relatório paginado com LWC e PaginationCursor

Uma aplicação prática muito comum é combinar o PaginationCursor com um componente LWC para criar uma tabela paginada que busca dados via Apex imperativo. Veja como montar essa integração.

// Apex Controller
public with sharing class AccountPaginationController {

    @AuraEnabled(cacheable=false)
    public static Map<String, Object> getAccountsPage(
        Integer startIndex, Integer pageSize
    ) {
        Database.PaginationCursor cursor = Database.getPaginationCursor(
            'SELECT Id, Name, Industry, AnnualRevenue FROM Account ORDER BY Name'
        );

        Database.CursorFetchResult result = cursor.fetchPage(startIndex, pageSize);

        Map<String, Object> response = new Map<String, Object>();
        response.put('records', result.getRecords());
        response.put('nextIndex', result.getNextIndex());
        response.put('isDone', result.isDone());
        response.put('deletedRows', result.getDeletedRows());

        return response;
    }
}
// LWC JavaScript
import { LightningElement, track } from 'lwc';
import getAccountsPage from '@salesforce/apex/AccountPaginationController.getAccountsPage';

export default class AccountPaginator extends LightningElement {
    @track accounts = [];
    startIndex = 0;
    pageSize = 10;
    isDone = false;
    isLoading = false;

    connectedCallback() {
        this.loadPage();
    }

    async loadPage() {
        this.isLoading = true;
        try {
            const result = await getAccountsPage({
                startIndex: this.startIndex,
                pageSize: this.pageSize,
            });
            this.accounts = [...this.accounts, ...result.records];
            this.startIndex = result.nextIndex;
            this.isDone = result.isDone;
        } catch (error) {
            console.error('Erro ao carregar registros:', error);
        } finally {
            this.isLoading = false;
        }
    }

    handleLoadMore() {
        if (!this.isDone && !this.isLoading) {
            this.loadPage();
        }
    }
}

Esse padrão de paginação incremental funciona especialmente bem em cenários onde o usuário precisa navegar por grandes volumes de dados sem carregar tudo de uma vez na tela. O PaginationCursor garante que a experiência permaneça fluida e que o tamanho das páginas seja sempre previsível.

Limites e exceções que todo desenvolvedor precisa conhecer

Apesar de serem extremamente poderosos, os Apex Cursors não escapam dos governor limits do Salesforce. Cada chamada de fetch() ou fetchPage() consome uma query SOQL e as linhas retornadas contam no limite de SOQL rows da transação. Além disso, existem limites diários específicos para cursors que é fundamental monitorar.

Para o Database.Cursor, o limite é de 50 milhões de linhas por cursor, 10.000 instâncias de cursor criadas por dia e 10.000 chamadas de fetch por cursor. Para o Database.PaginationCursor, o limite é de 100.000 linhas por cursor, 200.000 instâncias por dia e o tamanho máximo de página é 2.000 registros. Ambos compartilham um limite diário de linhas cursadas que pode ser monitorado via Limits.getApexCursorRows().

A Salesforce introduziu duas exceções específicas para cursors. A System.FatalCursorException indica um erro não recuperável e a transação deve ser abortada. A System.TransientCursorException sinaliza um problema temporário que permite retry seguro, tornando o cursor muito mais resiliente para processamentos longos que podem enfrentar instabilidades momentâneas.

// Monitorando limites de cursor em tempo de execução
System.debug('Cursors usados: ' + Limits.getApexCursors() + '/' + Limits.getLimitApexCursors());
System.debug('Linhas cursadas: ' + Limits.getApexCursorRows() + '/' + Limits.getLimitApexCursorRows());
System.debug('Chamadas fetch: ' + Limits.getFetchCallsOnApexCursor() + '/' + Limits.getLimitFetchCallsOnApexCursor());

// Consultando limites diários via OrgLimits
Map<String, System.OrgLimit> limitMap = OrgLimits.getMap();
System.OrgLimit dailyCursorLimit = limitMap.get('DailyApexCursorLimit');
System.debug('Cursors diários: ' + dailyCursorLimit.getValue() + '/' + dailyCursorLimit.getLimit());

System.OrgLimit dailyRowsLimit = limitMap.get('DailyApexCursorRowsLimit');
System.debug('Linhas cursadas diárias: ' + dailyRowsLimit.getValue() + '/' + dailyRowsLimit.getLimit());

Quando usar Cursor vs PaginationCursor vs Batch Apex

A escolha entre as três abordagens depende do cenário. O Database.Cursor é a melhor opção para processamento em background de grandes volumes com controle granular, especialmente quando combinado com Queueable encadeado. Ele substitui o Batch Apex em muitos casos, oferecendo mais flexibilidade e menos overhead.

O Database.PaginationCursor foi feito para situações de UI onde a consistência do tamanho da página é essencial. Se você está construindo uma tabela paginada ou um componente de busca que navega por resultados, essa é a classe certa. Ela lida graciosamente com deleções entre páginas e garante que o usuário sempre veja o número esperado de registros.

O Batch Apex continua sendo válido para processamentos agendados que precisam de uma estrutura formal de start/execute/finish, ou quando você precisa do Database.Stateful para manter estado entre lotes. Mas para muitos cenários modernos, especialmente aqueles que exigem reatividade e controle dinâmico, os Cursors são a escolha superior.

Conclusão: o futuro do processamento massivo no Apex

Os Apex Cursors representam uma evolução natural na forma como o Salesforce lida com grandes volumes de dados. Em vez de forçar o desenvolvedor a se encaixar nos moldes rígidos do Batch Apex, a plataforma agora oferece uma ferramenta que respeita a realidade de cada cenário: às vezes você precisa processar 5 milhões de registros em background, às vezes precisa paginar 500 resultados na tela, e em ambos os casos quer controle fino sobre o consumo de recursos.

Com o Cursor suportando até 50 milhões de linhas e o PaginationCursor garantindo consistência de páginas, desenvolvedores Salesforce ganham flexibilidade real para construir soluções de alta performance. Se você ainda não testou essas APIs no seu sandbox Spring ’26, agora é o momento. Comece com um cenário simples, substitua um Batch Apex existente por um Cursor + Queueable encadeado e compare a experiência. A diferença no controle e na performance vai te convencer.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *