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.
Siga el proceso con Trail Together
¿Desea seguir el proceso con un instructor a medida que realiza este paso? Eche un vistazo a este video, parte de la serie Trail Together en Trailhead Live.
(Este video comienza en el minuto 31:26, en caso de que desee rebobinar y mirar el comienzo del paso nuevamente).
Apex por lotes
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 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 usarDatabase.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:
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 } }
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.
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); } }
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 recopilación de todos los registros que el métodoexecute()
procesará en lotes individuales. Devuelve la lista de registros que se van a procesar mediante la llamada aDatabase.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étodoexecute()
establece la dirección de correo de cada contacto en la dirección de correo de las cuentas e incrementarecordsProcessed
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 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; } @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']); } }
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.
En el método test()
, 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 postal ‘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
- Guía del desarrollador de Apex: Apex por lotes
- Guía del desarrollador de Apex: Using Batch Apex
- Guía del desarrollador de Apex: Custom Iterators
- Guía del desarrollador de Apex: Anotación TestSetup