Empiece a realizar un seguimiento de su progreso
Inicio de Trailhead
Inicio de Trailhead

Usar Apex por lotes

Objetivos de aprendizaje

Después de completar esta unidad, tendrá conocimientos sobre lo siguiente:
  • Dónde usar Apex por lotes.
  • Límites superiores de Apex para el trabajo con lotes.
  • Sintaxis de Apex por lotes.
  • Mejores prácticas para Apex por lotes.

Batch Apex

Apex por lotes se usa para ejecutar trabajos de gran volumen (piense en miles o millones de registros), lo que superaría los límites de procesamiento normales. En el caso de Apex por lotes, puede procesar registros de forma asíncrona por lotes (de ahí el nombre “Apex por lotes”) dentro de los límites de la plataforma. Si tiene una gran cantidad de registros que procesar, por ejemplo para la limpieza y el archivado de datos, Apex por lotes es probablemente la mejor solución.

Veamos el funcionamiento subyacente de Apex por lotes. Supongamos que desea procesar 1 millón de registros con Apex por lotes. En este caso, se llama a la lógica de ejecución de la clase por lotes una sola vez para cada lote de registros que va a procesar. Cada vez que invoca una clase por lotes, el trabajo se incluye en la cola de trabajos de Apex y se ejecuta como una transacción discreta. Esta funcionalidad ofrece dos ventajas excepcionales:
  • Dado que cada transacción se inicia con un nuevo conjunto de límites regulados, es más fácil garantizar que el código permanezca dentro de los límites de ejecución regulados.
  • Si un lote no se procesa correctamente, las demás transacciones por lotes correctas no se revierten.

Sintaxis de Apex por lotes

Para escribir una clase de Apex por lotes, la clase debe implementar la interfaz Database.Batchable e incluir los tres métodos siguientes:

start

Se usa para recopilar los registros u objetos que se van a pasar al método de la interfaz execute para su procesamiento. A este método se llama una sola vez al inicio de un trabajo de Apex por lotes y devuelve el objeto Database.QueryLocator o bien un Iterable que contiene los registros u objetos pasados al trabajo.

En la mayoría de los casos, QueryLocator funciona con una consulta SOQL sencilla para generar el ámbito de los objetos del trabajo por lotes. No obstante, si necesita hacer algo más atrevido, como un bucle de paso por los resultados de una llamada a la API o procesar previamente los registros antes de su paso al método execute, puede desee consultar el vínculo sobre los iteradores personalizados de la sección Recursos.

Con el objeto QueryLocator, se omite el límite regulador para el número total de registros recuperados por consultas SOQL y puede consultar hasta 50 millones de registros. Sin embargo, con un Iterable, se debe aplicar el límite regulador para el número total de registros recuperados por consultas SOQL.

execute

Realiza el procesamiento real de cada sección o “lote” de datos pasados al método. El tamaño de lote predeterminado equivale a 200 registros. No se garantiza la ejecución de los lotes de registros en el orden en el que reciben del método start.

Este método requiere lo siguiente:
  • Una referencia al objeto Database.BatchableContext.
  • Una lista de sObjects, como List<sObject>, o una lista de tipos parametrizados. Si va a usar Database.QueryLocator, use la lista devuelta.
finish
Se usa para ejecutar operaciones posteriores al procesamiento (por ejemplo, envío de un mensaje de email) y se llama a este método una vez que se han procesado todos los lotes.

Esta es la estructura de una clase de Apex por lotes:

global class MyBatchClass implements Database.Batchable<sObject> {

    global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
        // collect the batches of records or objects to be passed to execute
    }

    global void execute(Database.BatchableContext bc, List<P> records){
        // process each batch of records
    }    

    global void finish(Database.BatchableContext bc){
        // execute any post-processing operations
    }    

}

Invocación de una clase por lotes

Para invocar una clase por lotes, solo tiene que crear una instancia de dicha clase y llamar a Database.executeBatch con la instancia:

MyBatchClass myBatchObject = new MyBatchClass(); 
Id batchId = Database.executeBatch(myBatchObject);

De forma opcional, también puede pasar un segundo parámetro scope para especificar el número de registros que se deben pasar al método de ejecución para cada lote. Sugerencia para profesionales: es posible que desee limitar el tamaño del lote si va a aplicar límites reguladores.

Id batchId = Database.executeBatch(myBatchObject, 100);

Cada invocación de Apex por lotes crea un registro AsyncApexJob para que pueda hacer un seguimiento del progreso del trabajo. Puede ver el progreso mediante SOQL o gestionar el trabajo en la cola de trabajos de Apex. Veremos cómo funciona la cola de trabajos en breve.

AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];

Uso del estado en Apex por lotes

Apex por lotes no suele tener ningún estado. Cada ejecución de un trabajo de Apex por lotes se considera una transacción discreta. Por ejemplo, un trabajo de Apex por lotes que contiene 1000 registros y usa el tamaño de lote predeterminado se considera equivalente a cinco transacciones de 200 registros cada una.

Si especifica Database.Stateful en la definición de clase, puede mantener el estado en todas las transacciones. Si se usa Database.Stateful, tan solo las variables de miembro de instancia conservan sus valores entre transacciones. Mantener el estado es útil para el conteo o resumen de los registros a medida que se procesan. En el siguiente ejemplo, vamos a actualizar los registros de contacto del trabajo por lotes y el objetivo es hacer un seguimiento del total de registros afectados para poder incluirlos en el email de notificación.

Código Apex por lotes de ejemplo

Ahora que ya sabe cómo escribir una clase de Apex por lotes, vamos a ver un ejemplo práctico. Supongamos que existe un requisito de negocio según el cual todos los contactos de compañías de Estados Unidos deben incluir la dirección de facturación de la compañía principal como su dirección de correo. Lamentablemente, los usuarios ingresan los nuevos contactos sin las direcciones correctas. ¡¿Acaso los usuarios no van a aprender nunca?! Escriba una clase de Apex por lotes que garantice el cumplimiento de este requisito.

La siguiente clase de ejemplo busca todos los registros de cuenta que se pasan con el método start() mediante QueryLocator y actualiza los contactos asociados con la dirección de correo de la cuenta correspondiente. Por último, envía un mensaje de email con los resultados del trabajo masivo y, dado que estamos usando Database.Stateful para hacer un seguimiento del estado, se actualiza el número de registros.

global class UpdateContactAddresses implements 
    Database.Batchable<sObject>, Database.Stateful {
    
    // instance member to retain state across transactions
    global Integer recordsProcessed = 0;

    global 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\''
        );
    }

    global 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;
    }    

    global 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);
    }    

}
El código debería ser bastante sencillo, pero puede ser un poco abstracto en realidad. A continuación detallamos el proceso:
  • El método start proporciona la colección de todos los registros que el método execute procesará en lotes individuales. Devuelve la lista de registros que se van a procesar mediante la llamada a Database.getQueryLocator con una consulta SOQL. En este caso, simplemente estamos consultando todos los registros de cuenta con ‘USA’ como país de facturación.
  • Cada lote de 200 registros se pasa en el segundo parámetro del método execute. El método execute establece la dirección de correo de cada contacto en la dirección de correo de las cuentas e incrementa recordsProcessed para hacer un seguimiento del número de registros procesados.
  • Una vez completado el trabajo, el método finish realiza una consulta en el objeto AsyncApexJob (una tabla con información sobre los trabajos por lotes) para obtener el estado del trabajo, la dirección de email del remitente y alguna otra información. A continuación, envía un email de notificación al remitente del trabajo en el que se incluye la información del trabajo y el número de contactos actualizados.

Prueba de Apex por lotes

Dado que el desarrollo y las pruebas de Apex por lotes están estrechamente relacionados, a continuación se describe cómo probar la clase por lotes anterior. En resumen, insertamos algunos registros, llamamos a la clase de Apex por lotes y, a continuación, confirmamos que los registros se han actualizado debidamente con la dirección correcta.

@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;
    }

    static testmethod 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']);
    }
    
}

El método setup inserta 10 registros de cuenta con la ciudad de facturación ‘New York’ y el país de facturación ‘USA’. Después, crea para cada cuenta un registro de contacto asociado. Estos datos los usa la clase por lotes.

Nota

Nota

Asegúrese de que el número de registros insertados es inferior al tamaño de lote de 200, ya que los métodos de prueba pueden ejecutar un solo lote en total.

En el método de prueba, se crea una instancia de la clase por lotes UpdateContactAddresses, la cual se invoca mediante la llamada a Database.executeBatch y el paso de la instancia de la clase por lotes.

La llamada a Database.executeBatch se incluye en el bloque de Test.startTest y Test.stopTest. Aquí es donde surte efecto la magia. El trabajo se ejecuta después de la llamada a Test.stopTest. El código asíncrono incluido en Test.startTest y Test.stopTest se ejecuta de forma asíncrona después de Test.stopTest.

Por último, la prueba permite confirmar si todos los registros de contacto se han actualizado correctamente. Para ello, se comprueba si el número de registros de contacto con la ciudad de facturación ‘New York’ coincide con el número de registros insertados (en este caso, 10).

Mejores prácticas

Como en el caso de los métodos futuros, hay una serie de aspectos que debe tener en cuenta cuando use Apex por lotes. Para garantizar la ejecución rápida de los trabajos por lotes, minimice el tiempo de llamada al servicio web y ajuste las consultas usadas en el código Apex por lotes. Cuanto más dura la ejecución del trabajo por lotes, más probabilidades hay de que otros trabajos de la cola se retrasen en caso de haber muchos trabajos en la cola. Las mejores prácticas incluyen lo siguiente:
  • Use Apex por lotes solo si tiene más de un lote de registros. Si no tiene registros suficientes para ejecutar más de un lote, probablemente lo mejor sea usar Apex para la inclusión en cola.
  • Ajuste cualquier consulta SOQL para recopilar los registros que se van a ejecutar lo más rápidamente posible.
  • Minimice el número de solicitudes asíncronas creadas para minimizar la probabilidad de retrasos.
  • Extreme la precaución si su plan es invocar un trabajo por lotes desde un desencadenador. Debe poder garantizar que el desencadenador no agregará un número de trabajos por lotes superior al límite.

Recursos

Nota

Nota

Recuerde, este módulo está pensado para Salesforce Classic. Cuando inicie su organización para realizar prácticas, cambie a Salesforce Classic para completar este reto.