Utilisation d'Apex par lot
Objectifs de formation
Dans cette unité, vous allez découvrir :
- L’emplacement d’utilisation d’un code Apex par lot
- Les limitations Apex lors de l’utilisation d’un lot Apex
- La syntaxe Apex par lot
- Les meilleures pratiques Apex par lot
Vidéo de démonstration Trail Together
Vous souhaitez être guidé pas à pas pendant que vous travaillez sur cette étape ? Regardez cette vidéo qui fait partie de la série Trail Together sur Trailhead Live.
(Ce clip commence à 31 min 26 s, au cas où vous voudriez revenir en arrière et regarder à nouveau le début de l’étape.)
Apex par lot.
Un code Apex par lot est utilisé pour exécuter les tâches volumineuses (des milliers ou des millions d’enregistrements !) qui dépasseraient les limitations de traitement normales. Le code Apex par lot permet de traiter les enregistrements de façon asynchrone dans des lots (d’où le nom « Apex par lot ») afin de respecter les limites de la plate-forme. Si vous avez un grand nombre d’enregistrements à traiter, notamment pour nettoyer ou archiver des données, l’utilisation d’un code Apex par lot est probablement la meilleure solution.
Voici comment fonctionne un code Apex par lot. Supposons que vous souhaitez traiter un million d’enregistrements en utilisant un code Apex par lot. La logique d’exécution de la classe par lot est appelée une fois pour chaque lot d’enregistrements que vous traitez. Chaque fois que vous invoquez une classe par lot, la tâche est placée dans la file d’attente des tâches Apex et exécutée en tant que transaction discrète. Cette fonctionnalité offre deux avantages importants :
- Chaque transaction initie une nouvelle série de limitations, ce qui aide votre code à respecter les limites d’exécution du gouverneur.
- Si le traitement d’un lot échoue, les autres transactions par lot réussies ne sont pas annulées.
Syntaxe d'Apex par lot
Pour écrire une classe Apex par lot, votre classe doit implémenter l’interface Database.Batchable et inclure les trois méthodes suivantes :
start()
Cette méthode est utilisée pour collecter les enregistrements ou les objets à transmettre pour traitement à la méthode execute(). La méthode start() est appelée une fois au début d’une tâche Apex par lot. Elle renvoie un objet Database.QueryLocator ou un Iterable (itérable) qui contient les enregistrements ou les objets transmis à la tâche.
La plupart du temps, un QueryLocator suffit avec une simple requête SOQL qui génère l’étendue des objets dans la tâche par lot. Cependant, si vous devez exécuter une tâche inhabituelle, par exemple itérer sur les résultats d’un appel API ou pré-traiter les enregistrements avant leur transmission à la méthode, consultez la page Itérateurs personnalisés.
Avec l’objet QueryLocator, les limitations du gouverneur en nombre total d’enregistrements récupérés par les requêtes SOQL sont contournées, et vous pouvez interroger jusqu’à 50 millions d’enregistrements. Néanmoins, avec un Iterable (itérable), les limitations du gouverneur en nombre total d’enregistrements récupérés par les requêtes SOQL restent applicables.
execute()
Cette méthode exécute le traitement pour chaque segment ou « lot » de données transmis à la méthode. La taille de lot par défaut est de 200 enregistrements. L’exécution des lots d’enregistrements dans l’ordre dans lequel ils sont transmis par la méthode start() n’est pas garantie.
La méthode execute() prend les paramètres suivants :
- Une référence à l’objet
Database.BatchableContext. - Une liste de sObjects, par exemple
List<sObject>, ou une liste de types paramétrés. Si vous utilisez unDatabase.QueryLocator, utilisez la liste renvoyée.
finish()
Cette méthode est utilisée pour exécuter des opérations de post traitement (par exemple, l’envoi d’un e-mail) et appelée lorsque tous les lots ont été traités.
La structure de base d’une classe Apex par lot se présente comme suit :
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
}
}Invocation d’une classe par lot
Pour invoquer une classe par lot, il suffit de l’instancier, puis d’appeler Database.executeBatch avec l’instance :
MyBatchClass myBatchObject = new MyBatchClass(); Id batchId = Database.executeBatch(myBatchObject);
Vous pouvez également transmettre un deuxième paramètre facultatif, scope, afin de spécifier le nombre d’enregistrements qui doivent être transmis dans la méthode execute correspondant à chaque lot. Conseil : limitez la taille de ce lot si vous rencontrez des limitations du gouverneur.
Id batchId = Database.executeBatch(myBatchObject, 100);
Chaque invocation du code Apex par lot crée un enregistrement AsyncApexJob qui permet de suivre la progression de la tâche. Vous pouvez afficher la progression via SOQL ou gérer votre tâche dans une file d’attente des tâches Apex. Nous reviendrons bientôt sur la file d’attente des tâches.
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];
Utilisation de l’état dans Apex par lot
Généralement, un code Apex par lot n’a pas d’état. Chaque exécution d’une tâche Apex par lot est considérée comme une transaction discrète. Par exemple, une tâche Apex par lot qui contient 1000 enregistrements et qui utilise la taille de lot par défaut est considérée comme cinq transactions de 200 enregistrements chacune.
Si vous spécifiez Database.Stateful dans la définition de la classe, vous pouvez conserver l’état à travers toutes les transactions successives. Lors de l’utilisation de Database.Stateful, seules les variables membres d’instance conservent leur valeur entre les transactions. La gestion de l’état est utile pour compter ou résumer les enregistrements à mesure qu’ils sont traités. Dans l’exemple suivant, nous mettons à jour les enregistrements de contact dans notre tâche par lot. Nous souhaitons garder une trace du nombre total d’enregistrements concernés afin de pouvoir les inclure dans l’e-mail de notification.
Exemple de code Apex par lot
Vous savez désormais comment écrire une classe Apex par lot. Prenons un exemple pratique. Supposons que votre cahier des charges exige que tous les contacts de sociétés aux États-Unis utilisent l’adresse de facturation de la société mère dans le champ Adresse postale. Malheureusement, les utilisateurs saisissent les nouveaux contacts sans renseigner la bonne adresse ! Écrivez une classe Apex par lot pour vous assurer que cette exigence est respectée.
La classe exemple ci-dessous recherche tous les enregistrements de compte transmis par la méthode start() à l’aide d’un QueryLocator et met à jour les contacts associés avec l’adresse postale de leur compte. Pour terminer, elle envoie un e-mail avec les résultats de la tâche par lot et, comme nous utilisons Database.Stateful pour suivre l’état, le nombre d’enregistrements mis à jour.
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);
}
}Détaillons le code :
- La méthode
start()fournit la collection de tous les enregistrements que la méthodeexecute()exécutera dans des lots individuels. Elle renvoie la liste des enregistrements à traiter en appelantDatabase.getQueryLocatoravec une requête SOQL. Dans le cas présent, nous interrogeons simplement tous les enregistrements Compte dans lesquels Pays de facturation comprend « USA ». - Chaque lot de 200 enregistrements est transmis au deuxième paramètre de la méthode
execute(). La méthodeexecute()définit l’adresse postale de chaque contact sur l’adresse postale du compte, et incrémenterecordsProcessedpour suivre le nombre d’enregistrements traités. - Une fois la tâche terminée, la méthode finish exécute une requête sur l’objet
AsyncApexJob(un tableau qui regroupe les informations sur les tâches par lot) en vue de récupérer le statut de la tâche, l’adresse e-mail de l’émetteur et d’autres informations. Elle envoie ensuite un e-mail de notification à l’émetteur de la tâche comprenant les informations sur la tâche et le nombre de contacts mis à jour.
Test d’un Apex par lot
Le développement et le test de code Apex allant de pair, examinons comment tester la classe par lot ci-dessus. En résumé, nous insérons quelques enregistrements de test, nous appelons la classe Apex par lot, puis nous affirmons que les enregistrements ont été correctement mis à jour avec l’adresse appropriée.
@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']);
}
}La méthode setup() est annotée avec @testSetup. Cette annotation vous permet de créer des enregistrements de test courants disponibles pour toutes les méthodes de la classe. Toutes les mises à jour des enregistrements de test sont annulées une fois l’exécution de chaque méthode de test terminée. Bien que cette classe de test n’inclue qu’une seule méthode (en plus de la méthode setup() elle-même), nous avons inclus l’annotation @testSetup ici à titre d’exemple. Dans ce cas, la méthode setup() insère 10 enregistrements de compte comprenant la ville de facturation « New York » et le pays de facturation « USA ». Elle crée ensuite un enregistrement de contact associé pour chaque compte. Ces données sont utilisées par la classe par lot.
Dans la méthode test(), la classe par lot UpdateContactAddresses est instanciée. Ensuite, cette instance de la classe par lot est transmise à Database.executeBatch. L’appel à Database.executeBatch est inclus dans le bloc Test.startTest et Test.stopTest, où la magie opère. La tâche est exécutée après l’appel à Test.stopTest. Tout code asynchrone inclus entre Test.startTest et Test.stopTest est exécuté de façon synchrone après Test.stopTest.
Pour terminer, le test vérifie que tous les enregistrements de contact ont été correctement mis à jour en vérifiant que le nombre d’enregistrements de contacts comprenant la ville postale « New York » correspond au nombre d’enregistrements insérés (c’est-à-dire 10).
Meilleures pratiques
Comme avec des méthodes futures, lors de l’utilisation d’un code Apex par lot, vous devez tenir compte de quelques points. Pour permettre l’exécution rapide des tâches par lots, réduisez les délais des appels externes de service Web et ajustez les requêtes utilisées dans votre code Apex par lot. Lorsque la file d’attente contient de nombreuses tâches, plus l’exécution d’une tâche par lot est longue, plus vous risquez de retarder les autres tâches.
Appliquez les meilleures pratiques :
- Utilisez un code Apex par lot uniquement si vous avez plusieurs lots d’enregistrements. Si vous n’avez pas suffisamment d’enregistrements pour exécuter plusieurs lots, il est préférable d’utiliser un code Apex pour file d’attente.
- Ajustez toutes les requêtes SOQL afin de regrouper aussi rapidement que possible les enregistrements à exécuter.
- Réduisez le nombre de requêtes asynchrones créées afin de réduire les risques de délai.
- Si vous envisagez d’invoquer une tâche par lot à partir d’un déclencheur, soyez extrêmement prudent(e). Assurez-vous que le nombre de tâches par lot ajoutées par le déclencheur ne dépasse pas la limite.
Ressources
- Guide du développeur Apex : Apex par lot.
- Guide du développeur Apex : Using Batch Apex
- Guide du développeur Apex : Custom Iterators
- Guide du développeur Apex : Annotation TestSetup
