Usar o Apex de lote
Objetivos de aprendizagem
Após concluir esta unidade, você saberá:
- Onde usar o Apex de lote.
- Os limites mais elevados do Apex ao usar lotes.
- A sintaxe do Apex de lote.
- As melhores práticas para o Apex de lote.
Acompanhar com o Trail Together
Deseja acompanhar um instrutor enquanto trabalha nesta etapa? Veja este vídeo que faz parte da série Trail Together no Trailhead Live.
(Este clipe começa na marca dos 31:26 minutos, caso você queira retroceder e ver o início da etapa novamente.)
Apex de lote
O Apex de lote é usado para executar trabalhos grandes (da ordem de milhares ou milhões de registros!) que excederiam os limites normais de processamento. Com o Apex de lote, você pode processar registros em lotes de forma assíncrona (daí o nome, “Apex de lote”) para permanecer dentro dos limites da plataforma. Se você tiver um lote de registros para processar, por exemplo, limpeza ou arquivamento de dados, provavelmente, o Apex de lote é sua melhor solução.
Aqui está como o Apex de lote funciona internamente. Digamos que você tenha de processar um milhão de registros usando o Apex de lote. A lógica de execução da classe de lote é chamada uma vez para cada lote de registros que estiver sendo processado. Toda vez que você invoca uma classe de lote, o trabalho é colocado na fila de trabalhos do Apex e é executado como uma transação discreta. Esta funcionalidade tem duas vantagens incríveis:
- Cada transação começa com um novo conjunto de limites do administrador, o que facilita garantir que seu código permanece dentro de tais limites de execução.
- Se um lote não for processado com sucesso, as outras transações bem-sucedidas do lote não serão revertidas.
Sintaxe do Apex de lote
Para escrever uma classe do Apex de lote, sua classe tem de implementar a interface Database.Batchable
e incluir os três métodos a seguir:
start
Usado para coletar os registros ou objetos a serem passados para o método de interface execute para processamento. Este método é chamado uma vez no início de um trabalho Apex de lote e retorna um objeto Database.QueryLocator
ou um Iterável
que contém os registros ou objetos passados para o trabalho.
A maioria das vezes, um QueryLocator
resolve a questão com uma simples consulta SOQL para gerar o escopo de objetos no trabalho em lote. Mas se você precisar fazer alguma coisa fora do comum, como um loop pelos resultados de uma chamada de API ou pré-processar registros antes de serem passados para o método execute, talvez seja melhor verificar o link dos Iteradores personalizados na seção Recursos.
Com o objeto QueryLocator
, o limite do administrador para o número total de registros recuperados pelas consultas SOQL é ignorado e você pode consultar até 50 milhões de registros. Porém, com um Iterável
, o limite do administrador para o número total de registros recuperados pelas consultas SOQL ainda está em vigor.
execute
Realiza o processamento real para cada bloco ou “lote” de dados passados para o método. O tamanho do lote padrão é de 200 registros. Não é garantido que os lotes de registros serão executados na sequência em que forem recebidos do método start.
Este método considera o seguinte:
- Uma referência para o objeto
Database.BatchableContext
. - Uma lista de sObjects, como
List<sObject>
ou uma lista de tipos parametrizados. Se você estiver usando umDatabase.QueryLocator
, use a lista retornada.
finish
Usado para executar operações de pós-processamento (por exemplo, enviar um email) e é chamado uma vez após todos os lotes serem processados.
Aqui está a estrutura de uma classe de Apex de lote:
public class MyBatchClass implements Database.Batchable<sObject> { public (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) { // collect the batches of records or objects to be passed to execute } public void execute(Database.BatchableContext bc, List<P> records){ // process each batch of records } public void finish(Database.BatchableContext bc){ // execute any post-processing operations } }
Como invocar uma classe de lote
Para invocar uma classe de lote, basta instanciá-la e chamar Database.executeBatch
com a instância:
MyBatchClass myBatchObject = new MyBatchClass(); Id batchId = Database.executeBatch(myBatchObject);
Se preferir, também é possível passar um segundo parâmetro scope para especificar o número de registros que devem ser transmitidos ao método de execução para cada lote. Dica especial: você pode querer limitar o tamanho deste lote se estiver executando dentro dos limites do administrador.
Id batchId = Database.executeBatch(myBatchObject, 100);
Cada invocação do Apex de lote cria um registro AsyncApexJob
para que você possa acompanhar o progresso do trabalho. Você pode ver o progresso via SOQL ou gerenciar seu trabalho na fila de trabalhos do Apex. Falaremos sobre a fila de trabalhos em breve.
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];
Como usar o estado no Apex de lote
Normalmente, o Apex de lote não tem estado. Cada execução de um trabalho do Apex de lote é considerada uma operação discreta. Por exemplo, um trabalho do Apex de lote que contenha 1.000 registros e use o tamanho de lote padrão representa cinco transações de 200 registros cada.
Se você especificar Database.Stateful
na definição de classe, você poderá manter o estado em todas as transações. Quando for usado o Database.Stateful
, somente as variáveis de membro da instância retêm seus valores entre as transações. Manter o estado é útil para a contagem ou o resumo dos registros enquanto eles são processados. Em nosso próximo exemplo, atualizaremos os registros de contatos em nosso trabalho de lote e queremos rastrear os registros totais afetados. Assim, podemos incluir a informação no email de notificação.
Amostra de código do Apex de lote
Agora que você já sabe como escrever uma classe do Apex de lote, vamos ver um exemplo prático. Digamos que você tenha um requisito comercial para que todos os contatos empresariais nos EUA tenham o endereço de faturamento de suas empresas principais como endereço de correspondência. Infelizmente, os usuários estão inserindo novos contatos sem os endereços corretos! Os usuários nunca vão aprender?! Escreva uma classe do Apex de lote que garanta que essa exigência é respeitada.
A classe de exemplo a seguir encontra todos os registros de conta que foram passados pelo método start()
usando um QueryLocator
e atualiza os contatos associados com seus endereços de correspondência da conta. Por fim, ela envia um email com os resultados do trabalho em massa e, por estarmos usando o Database.Stateful
para rastrear o estado, com o número de registros atualizado.
public class UpdateContactAddresses implements Database.Batchable<sObject>, Database.Stateful { // instance member to retain state across transactions public Integer recordsProcessed = 0; public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator( 'SELECT ID, BillingStreet, BillingCity, BillingState, ' + 'BillingPostalCode, (SELECT ID, MailingStreet, MailingCity, ' + 'MailingState, MailingPostalCode FROM Contacts) FROM Account ' + 'Where BillingCountry = \'USA\'' ); } public void execute(Database.BatchableContext bc, List<Account> scope){ // process each batch of records List<Contact> contacts = new List<Contact>(); for (Account account : scope) { for (Contact contact : account.contacts) { contact.MailingStreet = account.BillingStreet; contact.MailingCity = account.BillingCity; contact.MailingState = account.BillingState; contact.MailingPostalCode = account.BillingPostalCode; // add contact to list to be updated contacts.add(contact); // increment the instance member counter recordsProcessed = recordsProcessed + 1; } } update contacts; } public void finish(Database.BatchableContext bc){ System.debug(recordsProcessed + ' records processed. Shazam!'); AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email FROM AsyncApexJob WHERE Id = :bc.getJobId()]; // call some utility to send email EmailUtils.sendMessage(job, recordsProcessed); } }
O código deve ser bastante simples, mas pode ser um pouco abstrato. Aqui está o que acontece com mais detalhes:
- O método
start()
fornece a coleção de todos os registros que o métodoexecute()
processará em lotes individuais. Ele retorna a lista de registros a serem processados ao chamarDatabase.getQueryLocator
com uma consulta SOQL. Neste caso, estamos simplesmente consultando todos os registros de Account com o país de faturamento “USA”. - Cada lote de 200 registros é passado no segundo parâmetro do método
execute()
. O métodoexecute()
define o endereço de correspondência de cada contato ao endereço de faturamento da conta e incrementarecordsProcessed
para rastrear a quantidade de registros processados. - Quando o trabalho for concluído, o método finish executa uma consulta no objeto
AsyncApexJob
(uma tabela que lista informações sobre trabalhos em lote) para obter o status do trabalho, o endereço de email do remetente e algumas outras informações. Em seguida, esse método envia um email de notificação para o remetente do trabalho com informações sobre o trabalho e o número de contatos atualizados.
Como testar o Apex de lote
Uma vez que o desenvolvimento e o teste do Apex são realizados em conjunto, aqui está a forma como testamos a classe de lote acima. Resumindo, inserimos alguns registros, chamamos a classe Apex de lote e confirmamos se os registros foram atualizados com o endereço correto.
@isTest private class UpdateContactAddressesTest { @testSetup static void setup() { List<Account> accounts = new List<Account>(); List<Contact> contacts = new List<Contact>(); // insert 10 accounts for (Integer i=0;i<10;i++) { accounts.add(new Account(name='Account '+i, billingcity='New York', billingcountry='USA')); } insert accounts; // find the account just inserted. add contact for each for (Account account : [select id from account]) { contacts.add(new Contact(firstname='first', lastname='last', accountId=account.id)); } insert contacts; } @isTest static void test() { Test.startTest(); UpdateContactAddresses uca = new UpdateContactAddresses(); Id batchId = Database.executeBatch(uca); Test.stopTest(); // after the testing stops, assert records were updated properly System.assertEquals(10, [select count() from contact where MailingCity = 'New York']); } }
O método setup insere dez registros de conta com a cidade de faturamento “New York” e o país de faturamento “USA”. Então, para cada conta, ele cria um registro de contato associado. Estes dados são utilizados pela classe de lote.
No método test()
, a classe de lote UpdateContactAddresses
é instanciada, invocada ao chamar Database.executeBatch
e passada para a instância da classe de lote.
A chamada para o Database.executeBatch
é incluída no Test.startTest
e no bloco Test.stopTest
. É aqui que a magia acontece. O trabalho é executado após a chamada para o Test.stopTest
. Qualquer código assíncrono incluído no Test.startTest
e Test.stopTest
é executado em sincronia após o Test.stopTest
.
Por fim, o teste verifica se todos os registros de contato foram atualizados corretamente verificando se o número de registros de contatos com a cidade de correspondência “New York” corresponde ao número de registros inseridos (isto é, dez).
Melhores práticas
Tal como acontece com métodos futuros, existem algumas coisas que você deve manter em mente ao usar o Apex de lote. Para garantir a rápida execução de trabalhos em lotes, minimize o número de callouts de serviços da Web e ajuste as consultas usadas em seu código do Apex de lote. Quanto mais tempo demorar a execução do trabalho em lote, mais provável será que outros trabalhos serão atrasados quando houver muitos trabalhos na fila. As melhores práticas incluem:
- Use o Apex de lote somente se você tiver mais de um lote de registros. Caso não tenha registros o bastante para executar mais de um lote, será melhor usar o Apex, pois ele permite a execução em fila.
- Ajuste todas as consultas SOQL para coletar os registros, para que a execução seja efetuada o mais rápido possível.
- Minimize o número de solicitações assíncronas criadas para minimizar a possibilidade de atrasos.
- Tome muito cuidado se estiver planejando invocar um trabalho em lote a partir de um acionador. Você tem de garantir que o acionador não adicionará mais trabalhos em lote do que o limite.
Recursos
- Guia do desenvolvedor do Apex: Apex de lote
- Guia do desenvolvedor do Apex: Como usar o Apex de lote
- Guia do desenvolvedor do Apex: Iteradores personalizados
- Guia do desenvolvedor do Apex: Anotações do TestSetup