Apex assíncrono – Batch Apex – Parte 3

Dando continuidade a série de posts sobre Apex assíncrono, hoje vamos falar da Batch Apex, sem dúvida um dos mais complicados posts da série, e espero conseguir desmistificar e passar para você um overview bem bacana sobre ele, vamos lá? A mas se você ainda não leu o post sobre métodos futuros e sobre Queueable sugiro que de uma pausa na sua leitura, e leia esses posts antes :)

O que é uma Batch Apex

O Batch Apex é uma classe do Apex que implementa uma interface e dá ao desenvolvedor um poder de processamento enorme, permitindo que você enfileire inúmeros trabalhos do apex ao mesmo tempo, você pode ainda mover a ordem dos processos, obter o status do processamento, abordar o seu processamento e também realizar o agendamento, para que o seu processo seja executado em uma hora determinada por você.

Quando usar uma Batch Apex

  • Para tarefas de execução longa com grandes volumes de dados que precisam ser executados em lotes, como manutenção do banco de dados
  • Para tarefas que precisam de resultados de consulta maiores do que as transações regulares permitem

Estrutura de uma Batch Apex

Uma classe Batch Apex, deve implementar a interface Database.Batchable<X> onde X é o tipo de objeto que a sua Batch Apex irá iterar, se você não puder definir o tipo de metadado, pode especificar sObject, como todos os objetos herdam de sObject, você poderá iterar sobre eles sem problemas, fazendo um cast se necessário, mas a boa prática é que você informe o exato tipo de dados que esta trabalhando.

global class batchSample implements Database.Batchable<sObject> {

   global System.Iterable<sObject> start(Database.BatchableContext BC) {
      return null;
   }

   global void execute(Database.BatchableContext BC, List<sObject> scope) {
       //Processa o seu lote
    }

   global void finish(Database.BatchableContext BC) {
   }
}

Ao implementar a interface Database.Batchable, você precisará implementar 3 métodos em sua Batch Apex, são eles:

Start, o método start é o responsável por realizar a consulta inicial dos dados que serão processados pela sua Batch Apex, o retorno desse método deve ser obrigatoriamente um item que se possa iterar, um List, Array ou QueryLocator, o recomendado é um QueryLocator. O método Start recebe por parâmetro um BatchableContext, através desse parâmetro é possível obter o ID do Job e do Filho usando os métodos:

system.debug(BC.getChildJobId());

system.debug(BC.getJobId());

Execute, é aqui que a sua mágica acontece, nesse método você receberá o parâmetro BatchableContext e também um List<sObject> que será o seu escopo, por padrão a batch processa de 200 em 200 registros, ou seja a cada lote processado você receberá 200 registros, suponhamos que a sua consulta realizada no método Start tenha retornado 1.000 registros, nesse caso a sua Batch Apex irá criar 5 lotes, e eles serão processados de 200 em 200, caso seja necessário você poderá alterar o tamanho dos lotes, falaremos disso mais a frente, quando formos executar a nossa Batch Apex. Um ponto importante aqui é, se ocorrer um erro em um lote, os demais lotes ainda serão iniciados, independente dos erros dos lotes anteriores.

Finish, o método finish indica que todos os lotes foram processados, tenha sido eles com sucesso ou não, assim como no Start e no Execute, o finish também recebe por parâmetro o BatchableContext permitindo que você obtenha o Id do processo, por exemplo.

Algo muito interessante que podemos fazer no método Finish, é enviar um email ao Administrador, dizendo que o processamento foi finalizado, dessa forma poderíamos ter um código assim:

global void finish(Database.BatchableContext BC){
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

    AsyncApexJob a = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email
                    FROM AsyncApexJob 
                    WHERE Id =: BC.getJobId()];

    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    String[] toAddresses = new String[] {a.CreatedBy.Email};
    mail.setToAddresses(toAddresses);
    mail.setSubject('Batch Apex - ' + a.Status);
    mail.setPlainTextBody('The batch Apex job processed ' + a.TotalJobItems + ' batches with '+ a.NumberOfErrors + ' failures.');

    Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}

 

Executando uma Batch Apex

Você pode iniciar a execução de uma Batch Apex, através do Console do Desenvolvedor, através de um código do Apex ou até mesmo agendando sua execução, falaremos sobre esse último mais a frente. Para executarmos a nossa Batch Apex, precisamos executar o seguinte código Apex:

ID batchprocessid = Database.executeBatch(new batchSample());

AsyncApexJob aaj = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors
                    FROM AsyncApexJob 
                    WHERE ID =: batchprocessid];
System.debug(aaj);

O Database.executeBatch aceita 2 parâmetros, o primeiro é a instância da classe a ser executada, e o segundo (opcional) é o tamanho do lote, lembra que falamos dele lá em cima, então é aqui que podemos especificar a quantidade de registros que serão processadas por lote, se você não informar nada, o tamanho padrão é de 200 registros, quando o retorno for um Database.QueryLocator o valor máximo que você pode informar é de 2.000, se você tentar dar um de esperto e passar por exemplo 5.000, nesse caso ele continuará executando o máximo de 2.000.

E assim como as Classes Queueable, as Batches também retornam um ID único, que pode ser utilizado para consultar o status de processamento da Batch Apex.

Assim que uma Batch Apex é enfileirada, ela ganha o status Holding, podendo então seguir para os seguintes status:

Status Descrição
Holding O Job foi submetido e é mantido na fila flexível do Apex até que os recursos do sistema se tornem disponíveis para enfileirar o Job para processamento.
Queued O Job está aguardando sua execução.
Preparing o método Start do Job foi invocado. Esse status pode durar alguns minutos, dependendo do tamanho do lote de registros.
Processing O Job está sendo processado.
Aborted O Job foi abortado por um usuário.
Completed O Job foi concluído com ou sem falhas.
Failed O Job sofreu uma falha no sistema.

Encadeando uma Batch Apex

Em geral, a maioria das Batches que vejo, não colocam código nenhum no seu método Finish, mas é aqui que você pode fazer coisas realmente interessantes com as Batches, por exemplo fazer o encadeamento de Batches, ou seja, chamar outra Batch Apex para ser executada, ao contrário da Queueable que falamos no post anterior, as Batch Apex podem encadear outras Batches, sendo o único limite ao executar uma classe de teste.

Para encadear uma Batch Apex, basta chamar a execução de outra Batch no Finish da Batch em processamento, então ficaria assim:

global void finish(Database.BatchableContext BC) {
    if (!Test.isRunningTest()) {
        ID batchprocessid = Database.executeBatch(new SegundaBatchApex());
    }
}

Uma vez que você tenha o ID de sua Batch Apex, você poderá abortar a sua execução a qualquer momento, para isso será necessário fazer o uso do método System.AbortJob, você também pode fazer isso através da interface de usuário do Salesforce, procure por Trabalhos do Apex no menu de configurações, nessa tela será possível ver todos os seus processos que foram processados, e os que estão na fila aguardando ser processados, os que ainda estão na fila podem ser abortados a qualquer momento.

System.abortJob(batchprocessid);

Movendo a ordem de execução de uma Batch Apex usando a FlexQueue Class

Uma vez que sua Batch é enfileirada, você pode alterar a ordem de execução da mesma, você pode fazer isso através da interface de usuário do Salesforce, procure por Fila flexível do Apex no menu de configurações, ou ainda fazer isso usando a classe do Apex FlexQueue, você pode fazer isso de 4 formas diferentes, são elas:

moveAfterJob(jobToMoveId, jobInQueueId), você passa o ID do processo que quer mover, e o ID do processo na fila, o seu processo será executado DEPOIS do processo indicado.

moveBeforeJob(jobToMoveId, jobInQueueId), você passa o ID do processo que quer mover, e o ID do processo na fila, o seu processo será executado ANTES do processo indicado.

moveJobToEnd(jobId), nesse caso você passa apenas o ID do seu processo, e ele será movido para o FINAL da fila de execução.

moveJobToFront(jobId), aqui você também informa apenas o ID do seu processo, e ele será considerado prioridade, movendo-se para o PRIMEIRO LUGAR da fila de execução.

Todos esses métodos retornarão true caso o processo tenha sido movido com sucesso, e false, caso o processo não possa ser movido ou já esteja na posição indicada, ou seja não houve alteração da posição.

ID batchprocessid = Database.executeBatch(new batchSample());
System.debug(batchprocessid);

Boolean isSuccess = FlexQueue.moveJobToEnd(batchprocessid);
System.debug(isSucess);

O único ponto de atenção aqui, é que você só pode realizar esse processo enquanto o seu processo estiver com o status holding.

Callouts em uma Batch Apex

Assim como os métodos futuros e uma classe Queueable, as Batches também precisam ser informadas quando poderão ou não executar uma callout (chamada externa) e para isso devemos implementar mais uma interface do Apex, a mesma que usamos na classe Queueable, trata-se da interface Database.AllowsCallouts, dessa forma a declaração da nossa classe ficaria assim:

global class SampleBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
    ...
}

Após implementar essa interface será possível fazer as chamadas externas, mas tome cuidado, pois cada lote tem limites de chamadas.

Usando Database.Stateful para manter o estado entre lotes

Cada execução de um lote da Batch representa um novo estado transacional, e o que isso significa, bom de fato para a maioria das Batches, nada, mas existe alguns cenários onde manter o estado entre todos os lotes tem sua importância, inclusive resolvi essa semana um problema utilizando o Database.Stateful, o cenário era o seguinte, no start era realizado uma consulta SOQL que obtinha duas datas, e armazenava essas datas em variáveis locais, porém quando o método execute estava sendo executado, essas variáveis não tinham mais valor, invalidando o processo, para evitar que fosse feito uma nova consulta SOQL dentro do execute simplesmente implementamos a interface Database.Stateful e com isso conseguimos manter o estado das variáveis através de todos os lotes da Batch Apex que estava sendo executada.

Abaixo um exemplo hipotético, onde é listado todas as contas que foram criadas antes do atual ano fiscal, e com isso consulta todos os contatos dessas contas que foram criados no ano fiscal anterior, e em seguida remove esses contatos, a ideia aqui é somente mostrar um exemplo de uso, espero que sua imaginação te ajude a criar cenários melhores.

global class batchCenarioStatefull implements Database.Batchable<sObject>, Database.Stateful {
   global Date startDate, endDate;

   global System.Iterable<sObject> start(Database.BatchableContext BC) {
      FiscalYearSettings fiscalYear = [SELECT StartDate, 
            EndDate
            FROM FiscalYearSettings
            WHERE startDate = LAST_FISCAL_YEAR];

      startDate = fiscalYear.StartDate;
      endDAte = fiscalYear.EndDate;

      return [SELECT Id 
              FROM Account 
              WHERE CreatedDate <= :endDate];
   }

   global void execute(Database.BatchableContext BC, List<sObject> scope) {
      Set<Id> accSet = (new Map<Id, sObject>(scope)).keySet();

      List<Contact> res = [SELECT Id
            FROM Contact
            WHERE AccountId in :accSet
            AND CreatedDate >= :startDate
            AND CreatedDate <= :endDate];
      
      delete res;
   }

   global void finish(Database.BatchableContext BC) {
      //Envia um email dizendo que finalizou os processos
   }
}

Agendando uma Batch Apex

A essa altura, você já deve ter entendido todo o poder de processamento das Batch Apex, não é mesmo, mas de nada adianta todo esse poder de fogo se for necessário acessar o Console do Desenvolvedor ou criar um botão para executar sua Batch, o legal mesmo seria se ela fosse agendada para ser executada em uma determinada hora, não é mesmo, afinal imagina ter que acordar de madrugada para colocar um processo demorado para rodar, é para isso que server System.scheduleBatch, um método que nos permite agendar a execução de uma Batch Apex.

O System.scheduleBatch espera 3 parâmetros, o primeiro parâmetro é a instancia da Batch, o segundo é um nome de sua escolha, o terceiro e ultimo parâmetro é o tempo de espera em minutos após o agendamento em que a Batch será processada, suponhamos que você insira 60 nesse parâmetro, então a sua Batch será executada 1 hora depois de você chamar o método System.scheduleBatch, e o quarto e último parâmetro é opcional, e permite  que você especifique a quantidade de registros que será executada por lote, da mesma forma que fazemos quando executamos a batch, esse parâmetro tem o valor default de 200, e pode ir até 2.000, qualquer valor acima de 2.000 será considerado 2.000 quando estiver retornando dados usando Database.QueryLocator.

SampleBatch myInstance = new SampleBatch();

String cronID = System.scheduleBatch(myInstance, 'Sample Batch - Agendada', 1, 200);

CronTrigger ct = [SELECT Id, TimesTriggered, NextFireTime
                  FROM CronTrigger 
                  WHERE Id = :cronID];

// TimesTriggered deve ser 0, pois o processo ainda não foi executado
System.assertEquals(0, ct.TimesTriggered);

System.debug('Próxima execução em: ' + ct.NextFireTime); 

No nosso exemplo, ainda consultamos o agendamento, o objeto CronTrigger armazenará quando ele foi executado e qual a data da próxima execução, útil para por exemplo enviar um email para o administrador dizendo que o agendamento foi realizado, informando nesse caso a data em que ele será executado.

Testando uma Batch Apex

O segredo da execução dos testes, segue a mesma regra do que aplicamos para os métodos futuros e também para as classes Queueable, onde para que os métodos assíncronos sejam executados é necessário fazer o uso do Test.startTest() e Test.stopTest(), e sim, essa  é a única forma de garantir que os testes serão executados antes  do final do sua classe de teste, então a nossa classe de cobertura de Batch Apex ficará assim:

@isTest
private class testCover {
   @isTest
   static void testBatchApexCover() {
      Test.startTest();
      Database.executeBatch(new batchCenarioStatefull());
      Test.stopTest();
 
      //Verifica se os contatos foram excluídos
      Integer cct = [SELECT Count() FROM Contact WHERE CreatedDate < THIS_FISCAL_YEAR];
      
      System.assertEquals(0, cct);
   }

   @testSetup 
   static void setup() {
       Account a = new Account(Name = 'Test');
       insert a;
       
       //Altera data de criação do objeto
       Test.setCreatedDate(a.Id, Datetime.now().addYears(-3));
       List<Contact> contacts = new List<Contact>();

       for(Integer i=0; i< 10; i++) {
           Contact c = new Contact();
           c.FirstName = 'Nome ' + i;
           c.LastName = 'Sobrenome ' + i;
           c.AccountId = a.Id;
           c.Email = 'nome'+i+'@acme.com';

           contacts.add(c);
       }

       insert contacts;
       
       for(Contact c :contacts) {
           //Altera data de criação do objeto
           Test.setCreatedDate(c.Id, Datetime.now().addYears(-3)); 
       }
   }
}

Note que aqui usamos um cara especial, que vale um pouco de atenção, pois acredito que poderá te ajudar em outras classes de testes, que é a opção de especificar  o CreatedDate de um registro inserido na classe de teste, esse método é o Test.setCreatedDate  que recebe basicamente dois parâmetros, o primeiro é o Id do registro e o segundo a data que você quer especificar como data de criação do registro, sem esse método seria impossível testar nossa Batch Apex, pois ela trabalha especificamente com registros  antigos.

Limites de uma Batch Apex

Quem  nunca esbarrou em um limite do Salesforce que atire a primeira pedra, sim eles existem e eu acredito que seja para o nosso próprio bem, sem eles eu acredito que o Salesforce não teria todo o poder de processamento que tem hoje, os principais limites que devemos ter em mente para a execução de uma Batch Apex são:

  • Até 5 trabalhos podem ser executados simultaneamente.
  • Até 100 trabalhos em lote podem ser mantidos na fila flex do Apex.
  • Em uma classe de teste, você pode enfileirar no máximo 5 trabalhos em lote.
  • O número máximo de execuções do método Apex em lote por período de 24 horas são de 250.000, ou o número de licenças de usuário em sua organização multiplicado por 200, deve ser considerado o que for maior.
    • As execuções de métodos incluem execuções dos métodos start, execute e finish. Esse limite é para toda sua organização e é compartilhado com todos os Apex assíncronos: Apex em lote, Apex em execução, Apex agendado e métodos futuros.
  • O método start da sua Batch Apex pode conter até 15 cursores de consulta abertos por vez, por usuário.
    • Os métodos execute e finish tem cada um um limite de 5 cursores de consulta abertos por usuário.
  • Um máximo de 50 milhões de registros pode ser retornado em um Database.QueryLocator. Se mais de 50 milhões de registros forem retornados, o trabalho em lotes será encerrado imediatamente e marcado como Falha.
  • Se o método start da Batch Apex retorna um QueryLocator, o parâmetro de escopo opcional do Database.executeBatch ter um valor máximo de 2.000.
    • Se configurado para um valor mais alto, o Salesforce irá considerar  somente  2.000 (Não tente ser mais esperto do que eles).
    • Se você utilizar outra forma de iteração, como Listo ou Array você poderá informar valores acima de 2.000, mas tome muito cuidado, se você usar um número muito alto, poderá entrar em outros limites.
  • Se nenhum tamanho for especificado com o parâmetro de escopo opcional de Database.executeBatch, O Salesforce agrupa os registros retornados pelo método start em lotes de 200.
  • Apenas um método start pode ser executado por vez em uma organização. Trabalhos em lote que ainda não foram iniciados permanecem na fila até serem iniciados. Observe que esse limite não faz com que nenhum trabalho em lote falhe, os métodos execute das Batches ainda são executados em paralelo se mais de um trabalho estiver em execução.

Boas práticas ao usar uma Batch Apex

  • Tome muito cuidado se estiver planejando invocar um trabalho em lotes de uma Trigger. Você deve ser capaz de garantir que a Trigger não adicione mais trabalhos em lote do que o limite. Em particular, considere atualizações que podem vir de um Bulk API, assistentes de importação, alterações de registro em massa por meio da interface do usuário e todos os casos em que mais de um registro pode ser atualizado por vez.
  • Quando Você chamar o Database.ExecuteBatch, O Salesforce apenas coloca o trabalho na fila. A execução real pode ser atrasada com base na disponibilidade do serviço.
  • Ao testar sua Batch Apex, você pode testar apenas uma execução do método execute. Use o parâmetro de escopo do executeBatch para limitar o número de registros passados ​​para o método execute para garantir que você não está executando limites superiores  ao permitido.
  • o método executeBatch inicia um processo assíncrono. Quando você testar o Apex em lote, certifique-se de que a tarefa em lote processada de forma assíncrona esteja concluída antes de testar os resultados. Use os métodos de Test.startTest() e test.stopTest() para para garantir que ele termine antes de continuar seu teste.
  • Use Database.Stateful na declaração da  sua classe se você quiser compartilhar variáveis ​​ou dados de membros da instância em transações do lote. Caso contrário, todas as variáveis ​​de membro serão redefinidas para seu estado inicial no início de cada transação.
  • Métodos declarados com @future não são permitidos em classes que implementam a interface Database.Batchable.
  • Métodos declarados com @future não pode ser chamado de uma classe Batch Apex.
  • Quando uma tarefa em lote do Apex é executada, as notificações por email são enviadas ao usuário que enviou a tarefa em lote. Se o código estiver incluído em um pacote gerenciado e a organização de inscrição estiver executando o trabalho em lote, as notificações serão enviadas ao destinatário listado no campo Destinatário de notificação de exceção do Apex .
  • Cada chamada do Apex em lote cria uma registro do objeto AsyncApexJob. Para construir uma consulta SOQL para recuperar o status da tarefa, o número de erros, o progresso e o remetente, use o AsyncApexJobId do registro.
  • Todos os métodos da classe devem ser definidos como global ou public.
  • Minimize o número de lotes sempre  que possível. O Salesforce usa uma estrutura baseada em fila para lidar com processos assíncronos de fontes como métodos futuros e Apex em lote. Essa fila é usada para balancear a carga de trabalho de solicitação entre organizações. Se mais de 2.000 solicitações não processadas de uma única organização estiverem na fila, todas as solicitações adicionais da mesma organização serão atrasadas enquanto a fila manipula solicitações de outras organizações.
  • Assegure-se de que as tarefas em lote sejam executadas o mais rápido possível. Para garantir a execução rápida de tarefas em lote, minimize os tempos de callout do serviço da Web e ajuste as consultas usadas no código Apex em lote. Quanto mais tempo o trabalho em lote for executado, mais provavelmente outros trabalhos em fila serão atrasados ​​quando muitos trabalhos estiverem na fila.
  • As tarefas em lote do Apex são executadas mais rapidamente quando o método start retorna um objeto Database.QueryLocator que não inclui registros relacionados por meio de uma sub-consulta. Evitando sub-consultas de relacionamento em um Database.QueryLocator permite que tarefas em lote sejam executadas usando uma implementação mais rápida e fragmentada. Se o método start retorna um iterável ou um objeto Database.QueryLocator com uma sub-consulta de relacionamento, a tarefa em lote usa uma implementação mais lenta, sem segmentação. Por exemplo, se a consulta a seguir for usada como retorno de um Database.QueryLocator, o trabalho em lote usa uma implementação mais lenta devido à sub-consulta de relacionamento: SELECT Id, (SELECT id FROM Contacts) FROM Account
    Uma estratégia melhor é executar a sub-consulta separadamente, a partir do método de execução, que permite que a tarefa em lote seja executada usando a implementação mais rápida e em blocos.

 

Apesar desse post ter ficado um pouco extenso, o assunto é muito importante, pois o uso de Batches no dia a dia de uma ORG é muito comum, então se você é um desenvolvedor, deve estar familiarizado com esse assunto, e se você é um Administrador, nada melhor do que saber como as coisas funcionam, não é mesmo, aproveita que você chegou até aqui e me diga nos comentários o que esta achando dessa série de posts e se a sua ORG usa Batch Apex em algum processo da empresa, no nosso próximo post falarei sobre o Schedule.

Um forte abraço trailblazer e até o próximo post :)

 

Fernando Sousa

Senior Salesforce Developer

Bacharel em Sistemas da Informação pela Universidade de Taubaté (UNITAU) e MBA em Projeto de Aplicações para Dispositivos Móveis pelo IGTI – Instituto de Gestão em Tecnologia da Informação. 

Comecei a programar bem cedo, por volta de 10 anos de idade, de maneira auto-didata passei por várias linguagens.

Em 2015 me conectei a plataforma Salesforce pela primeira vez, para fazer una integração entre um Aplicativos Mobile em android e o Salesforce Platform. 

Atualmente com as certificações Salesforce Certified Platform Developer I, Salesforce Certified Platform App BuilderSalesforce Certified Platform Developer IISalesforce Administrator e Sharing and Visibility.

Acompanhe meu Trailhead aqui.

 

ApexApex AssíncronoBatch ApexBatchesFlexQueueSalesforceSalesforce Developer