Développer un batch en Apex dans Salesforce

Aurélien Laval

Comment développer, tester et planifier un batch dans Salesforce.com ?

Commentez

- Titre : Développer un batch en Apex dans Salesforce

- Auteur : Aurélien Laval

- Parution : 1 décembre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Aurélien Laval. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Prérequis

- III. Définition et fonctionnement d'un batch

- IV. Développer un batch

- V. Tester un batch

- VI. Planifier l'exécution d'un batch

- VII. Conclusion

- VIII. Ressources utiles

- IX. Remerciements

I - Introduction

Il peut arriver (dans Salesforce.com ou d'autres systèmes d'information) de devoir exécuter un traitement asynchrone sur des données (de petite ou grande quantité) pour les archiver, les nettoyer ou pour un autre besoin.

Le batch répond tout à fait à ce besoin puisqu'il s'agit ni plus ni moins d'une tâche CRON que l'on retrouve sur d'autres systèmes informatisés plus communs tels que Unix ou Windows.

Avant d'aller plus loin dans cet article, je vous propose quelques rappels sur les termes techniques qui vont être utilisés :

- Salesforce.com est un CRM (Customer Relationship Manager) orienté SaaS (Software as a Service) et PaaS (Platform as a Service) permettant la gestion des relations clients d'une entreprise ;

- Apex, est un langage de programmation côté serveur permettant de modifier la logique métier et le traitement des données ;

- SOQL (Salesforce Object Query Language) est le langage permettant d'effectuer des requêtes dans la base de données de Salesforce.com.

Maintenant, place au scénario de ce tutoriel : nous souhaitons mettre à jour toutes les nuits, le nombre de points des comptes en fonction des événements commerciaux auxquels ils ont participé.

C'est-à-dire que si un compte participe à un événement commercial, cela lui rapporte un certain nombre de points suivant son niveau (bronze, argent ou or) et l'année de l'événement en question.

Un compte peut donc participer à un ou plusieurs événements commerciaux et un événement commercial peut être suivi par un ou plusieurs comptes.

Voici donc le schéma du modèle de données que je propose :

Il peut y avoir des notions que vous ne comprenez pas dans ce tutoriel, mais pour éviter de faire de la redondance, je vous invite à vous reporter vers la section « Ressources utiles ».

II - Prérequis

Avant toute chose, vous devez disposer d'une instance afin de pouvoir suivre ce tutoriel. Si ce n'est pas le cas, je vous invite à vous rendre à cette adresse. Salesforce met à disposition plusieurs types d'instances qui proposent différentes fonctionnalités et donc à différents prix (prix par licence utilisateur par mois). La seule instance que vous pourrez généreusement obtenir de la part de Salesforce est celle de développement (celle que je vous propose via le lien ci-dessus) qui, comme son nom l'indique, sert à tester et développer des services. Je vous laisse juger par vous-même la grille tarifaire sur les diverses instances que propose le CRM.

Vous aurez également besoin d'un environnement de développement pour écrire votre code, vous pouvez utiliser celui fourni par Salesforce ou alors vous procurer celui compatible avec Eclipse et qui s'appelle Force.com IDE (je l'utiliserai tout au long de ce tutoriel). Pour ce dernier, rendez-vous sur l'Eclipse MarketPlace et installez-le.

III - Définition et fonctionnement d'un batch

Un batch est une classe Apex qui lorsqu'elle est appelée se place à la fin d'une file d'attente (queue en anglais) pour qu'elle puisse être exécutée lorsque cela sera possible (en fonction du nombre de traitements à effectuer).

Vous pouvez apercevoir vos planifications dans Salesforce.com en vous rendant à Setup => Monitoring => Scheduled Jobs et la file d'attente dans Setup => Monitoring => Apex Jobs.

Les batchs traitent par défaut des lots de 200 enregistrements (mais il est possible de spécifier une autre valeur entre 1 et 10 000 lors de son exécution). Ce qui signifie que si nous voulons des lots de 200 enregistrements et que nous avons 500 enregistrements à traiter, nous appellerons donc trois fois le batch. Il faut savoir qu'à chaque appel du batch, les governor limits sont réinitialisées, cela permet une plus grande souplesse sur les limites qu'impose Salesforce.com.

Par limites, on peut y trouver :

- 100 requêtes SOQL lors d'une exécution synchrone (contre 200 en asynchrone) ;

- 10 000 enregistrements maximum retournés lors de l'exécution de la requête SOQL de la méthode DataBase.getQueryLocator() ;

- 120 secondes maximum avant un temps mort lors d'un appel à un service web ou d'une requête HTTP ;

- 10 secondes de temps CPU allouées lors d'une transaction APEX (lors d'un traitement côté serveur) ;

- 50 appels à des méthodes annotées future lors d'une transaction APEX.

Il existe plein d'autres limites qu'il peut être appréciable de connaître pour développer.

Je vous partage le lien vers la documentation concernant ces limites dans la partie VIII Ressources utiles.

IV - Développer un batch

Lors de la création d'un batch, il faut respecter certaines conditions pour que celui-ci soit valide, la classe doit au minimum :

- implémenter l'interface Database.Batchable ;

- contenir les méthodes start(), execute() et finish().

Dans Salesforce.com, chaque lot d'enregistrements d'un batch est considéré comme une transaction discrète (discrete transaction en anglais), cela vous permet, si vous spécifiez l'implémentation de l'interface Database.Stateful, de garder l'état de vos variables durant le traitement de tous les lots, elles ne seront jamais réinitialisées, cela peut être pratique pour des variables booléennes ou pour des map contenant des enregistrements.

Autre point également important à connaître, si un lot d'enregistrements échoue pour une raison ou une autre, les autres lots ne seront pas impactés et suivront leur déroulement prévu.

Maintenant, commençons par créer notre batch avec son minimum vital :

1. global class BatchEventsAccountPointsCalculation implements Database.Batchable<sObject>, Database.Stateful{

3. public String query;

5. global Database.QueryLocator start(Database.BatchableContext bc){

7. return Database.getQueryLocator(query);

8. }

10. global void execute(Database.BatchableContext BC, List<sObject> scope){

12. }

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

16. }

17. }

La méthode start() est utilisée pour charger les données sur lesquelles nous effectuerons le traitement, dans notre cas, il s'agit des comptes.

Nous devons retourner la méthode Database.getQueryLocator() en lui fournissant en paramètre, la requête SOQL (Salesforce Object Query Language) permettant de requêter les comptes au format String.

La méthode execute() quant à elle, permet d'effectuer tout le traitement nécessaire sur les enregistrements que l'on vient de récupérer à l'aide de la requête dans la méthode start() en l'occurrence, des comptes.

Passons maintenant à la dernière méthode, finish(). Elle est appelée en dernière lorsque le traitement de execute() est terminé.

Nous pouvons utiliser cette méthode pour envoyer des mails de confirmation du traitement à des utilisateurs, appeler un autre batch ou encore autre chose.

Afin de faciliter au maximum la maintenance du code, il est très préférable que lorsque vous avez besoin d'utiliser des valeurs fixes (qui ne dépendent pas d'enregistrements) comme du texte ou des nombres, d'utiliser des labels personnalisés (custom labels) dans Salesforce.com. Cela facilite la maintenance et dans le cas où par exemple, je remplace le niveau des comptes silver en autre chose, un custom label permet une modification rapide et propre et ne nécessite pas une nouvelle mise en production (change set, exécution des classes de test, etc.) vu qu'il n'y a pas de modification du code.

Dans ce tutoriel, pour savoir quels points attribuer aux comptes, je dois savoir de quel type ils sont (argent, gold ou platinium) et comme ce sont des valeurs fixes, j'ai créé des labels que j'utilise dans le batch (au travers de variables en les récupérant avec System.Label.nom_de_mon_label).

Voici l'algorithme que je propose.

Il nous faut récupérer les informations des comptes (avec au minimum leur identifiant et leur niveau) ainsi que tous les événements auxquels ils ont participé.

Une fois que nous avons les comptes, nous itérons dessus dans un premier temps, puis sur leurs événements pour leur attribuer les points suivant leur niveau.

Voici le résultat :

1. /** Batch qui calcule les points de participation à des événements commerciaux des comptes **/

2. global class BatchEventsAccountPointsCalculation implements Database.Batchable<sObject>, Database.Stateful{

4. public String query;

5. private Integer currentYear;

6. private List<Account> accountList;

7. private String silverLabel = System.Label.Label_silver;

8. private String goldLabel = System.Label.Label_gold;

9. private String platiniumLabel = System.Label.Label_platinium;

11. global Database.QueryLocator start(Database.BatchableContext bc){

13. this.accountList = new List<Account>();

15. this.currentYear = Date.today().year();

17. this.query = 'SELECT Id, Name, Level__c, Points__c, (SELECT Id, Account__c, Events_Business_Points__r.Silver_level_points__c, Events_Business_Points__r.Gold_level_points__c, Events_Business_Points__r.Platinium_level_points__c, Events_Business_Points__r.Year__c FROM Participations__r WHERE Events_Business_Points__r.Year__c = :currentYear) FROM Account WHERE Level__c != NULL';

19. return Database.getQueryLocator(this.query);

20. }

22. global void execute(Database.BatchableContext BC, List<sObject> scope){

24. // Pour chaque compte

25. for(sObject aScope : scope){

27. Account anAccount = (Account) aScope;

29. Decimal nbPoints = 0;

31. // Pour chaque participation du compte

32. for(Participation__c aParticipation : anAccount.Participations__r){

34. // Si compte silver

35. if(anAccount.Level__c.equals(silverLabel) && aParticipation.Events_Business_Points__r.Silver_level_points__c != null){

36. nbPoints += aParticipation.Events_Business_Points__r.Silver_level_points__c;

37. }

38. // Sinon si compte gold

39. else if(anAccount.Level__c.equals(goldLabel) && aParticipation.Events_Business_Points__r.Gold_level_points__c != null){

40. nbPoints += aParticipation.Events_Business_Points__r.Gold_level_points__c;

41. }

42. // Sinon si compte platinium

43. else if(anAccount.Level__c.equals(platiniumLabel) && aParticipation.Events_Business_Points__r.Platinium_level_points__c != null){

44. nbPoints += aParticipation.Events_Business_Points__r.Platinium_level_points__c;

45. }

46. }

48. anAccount.Points__c = nbPoints;

50. this.accountList.add(anAccount);

51. }

53. // Mise a jour des comptes

54. update this.accountList;

55. }

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

59. }

60. }

V - Tester un batch

Comme tout développement mérite sa classe de test, nous sommes obligés pour nous assurer que notre batch fonctionne correctement, de le tester.

Pour cela, nous recréons un jeu de données qui va nous servir à le tester.

Il faut le plus possible éviter d'utiliser les données de l'instance.

Nous aurons donc besoin de créer :

- des comptes ;

- des événements commerciaux ;

- des points pour les événements commerciaux par année ;

- des participations des comptes à des événements commerciaux.

Comme il peut parfois y avoir besoin d'effectuer plusieurs tests pour valider une classe Apex (tester avec plusieurs contextes), je recommande de concevoir une méthode d'initialisation (init() dans notre classe de test) afin d'initialiser notre environnement de test et de factoriser le code et son éventuelle maintenance.

Grâce à ça, vous n'aurez seulement qu'à appeler cette méthode dans les tests et vos données seront prêtes.

Maintenant que nous avons nos données, nous devons appeler notre batch pour qu'il s'exécute, mais nous devons le lancer à l'intérieur des balises Test.startTest() et Test.stopTest() afin que les limites imposées par Salesforce.com (je vous en ai parlé un peu plus haut) puissent être prises en compte.

Une fois le batch exécuté, nous récupérons nos comptes à partir de Salesforce.com avec leurs nouveaux points pour vérifier qu'ils ont bien le bon nombre et que notre batch est par conséquent, bien développé et éviter tout problème une fois mis en production avec de vraies données :

1. /** Classe de test du batch «  BatchEventsAccountPointsCalculation » **/

2. @isTest

3. private class BatchEventsAccountPointsCalculation_TEST {

5. static List<Account> accountList;

6. static List<Business_Event__c> businessEventList;

7. static List<Events_Business_Points__c> eventBusinessPointList;

8. static List<Participation__c> participationList;

9. static String silverLabel = System.Label.Label_silver;

10. static String goldLabel = System.Label.Label_gold;

11. static String platiniumLabel = System.Label.Label_platinium;

12. static Date todayDate;

14. static void init(){

15. todayDate = Date.today();

17. /** Account **/

18. accountList = new List<Account>();

19. accountList.add(new Account(

20. Name = 'Test name silver',

21. Level__c = silverLabel

22. ));

23. accountList.add(new Account(

24. Name = 'Test name gold',

25. Level__c = goldLabel

26. ));

27. accountList.add(new Account(

28. Name = 'Test name platinium',

29. Level__c = platiniumLabel

30. ));

32. insert accountList;

34. /** Business_Event__c **/

35. businessEventList = new List<Business_Event__c>();

36. businessEventList.add(new Business_Event__c(

37. Name = 'Salesforce1 World Tour Paris',

38. Place__c = 'Paris'

39. ));

40. businessEventList.add(new Business_Event__c(

41. Name = 'Dreamforce',

42. Place__c = 'Los Angeles'

43. ));

44. businessEventList.add(new Business_Event__c(

45. Name = 'Salesforce1 World Tour London',

46. Place__c = 'London'

47. ));

49. insert businessEventList;

51. /** Events_Business_Points__c **/

52. eventBusinessPointList = new List<Events_Business_Points__c>();

53. eventBusinessPointList.add(new Events_Business_Points__c(

54. Business_Event__c = businessEventList[0].Id,

55. Year__c = todayDate.year()-1,

56. Silver_level_points__c = 10,

57. Gold_level_points__c = 15,

58. Platinium_level_points__c = 30

59. ));

60. eventBusinessPointList.add(new Events_Business_Points__c(

61. Business_Event__c = businessEventList[0].Id,

62. Year__c = todayDate.year(),

63. Silver_level_points__c = 20,

64. Gold_level_points__c = 35,

65. Platinium_level_points__c = 40

66. ));

67. eventBusinessPointList.add(new Events_Business_Points__c(

68. Business_Event__c = businessEventList[1].Id,

69. Year__c = todayDate.year()-1,

70. Silver_level_points__c = 100,

71. Gold_level_points__c = 150,

72. Platinium_level_points__c = 300

73. ));

74. eventBusinessPointList.add(new Events_Business_Points__c(

75. Business_Event__c = businessEventList[1].Id,

76. Year__c = todayDate.year(),

77. Silver_level_points__c = 150,

78. Gold_level_points__c = 300,

79. Platinium_level_points__c = 450

80. ));

81. eventBusinessPointList.add(new Events_Business_Points__c(

82. Business_Event__c = businessEventList[2].Id,

83. Year__c = todayDate.year(),

84. Silver_level_points__c = 120,

85. Gold_level_points__c = 270,

86. Platinium_level_points__c = 430

87. ));

89. insert eventBusinessPointList;

91. /** Participation__c **/

92. participationList = new List<Participation__c>();

93. participationList.add(new Participation__c(

94. Account__c = accountList[0].Id,

95. Events_Business_Points__c = eventBusinessPointList[0].Id

96. ));

97. participationList.add(new Participation__c(

98. Account__c = accountList[0].Id,

99. Events_Business_Points__c = eventBusinessPointList[1].Id

100. ));

101. participationList.add(new Participation__c(

102. Account__c = accountList[1].Id,

103. Events_Business_Points__c = eventBusinessPointList[4].Id

104. ));

105. participationList.add(new Participation__c(

106. Account__c = accountList[1].Id,

107. Events_Business_Points__c = eventBusinessPointList[0].Id

108. ));

109. participationList.add(new Participation__c(

110. Account__c = accountList[2].Id,

111. Events_Business_Points__c = eventBusinessPointList[1].Id

112. ));

113. participationList.add(new Participation__c(

114. Account__c = accountList[2].Id,

115. Events_Business_Points__c = eventBusinessPointList[2].Id

116. ));

117. participationList.add(new Participation__c(

118. Account__c = accountList[2].Id,

119. Events_Business_Points__c = eventBusinessPointList[4].Id

120. ));

122. insert participationList;

123. }

125. /** Teste les points du compte **/

126. static testMethod void testAccountPoint() {

128. init();

130. Test.startTest();

132. Database.executeBatch(new BatchEventsAccountPointsCalculation());

134. Test.stopTest();

136. // Récupération des comptes dans Salesforce

137. List<Account> newAccountList = [

138. SELECT Id, Name, Points__c

139. FROM Account

140. WHERE Id IN :accountList

141. ];

143. // Verification des points

144. System.assertEquals(eventBusinessPointList[1].Silver_level_points__c, newAccountList[0].Points__c);

145. System.assertEquals(eventBusinessPointList[4].Gold_level_points__c, newAccountList[1].Points__c);

146. System.assertEquals((eventBusinessPointList[1].Platinium_level_points__c + eventBusinessPointList[4].Platinium_level_points__c), newAccountList[2].Points__c);

147. }

148. }

VI - Planifier l'exécution d'un batch

Développer un batch, c'est bien, mais il nous faut maintenant créer une classe qui va nous permettre de l'exécuter.

Pour ce faire, la classe doit obligatoirement implémenter l'interface Schedulable, mais seulement comporter l'exécution du batch en utilisant la méthode Database.executeBatch().

Cette méthode attend deux paramètres :

- une instance du batch ;

- (optionnel) le nombre d'enregistrements maximum par lot (entre 1 et 10 000) que nous voulons, si ce n'est pas spécifié, la valeur de 200 est prise par défaut.

1. global class ScheduleBatchEventsAccountPointsCalc implements Schedulable{

3. global void execute(SchedulableContext ctx) {

5. Database.executeBatch(new BatchEventsAccountPointsCalculation());

7. }

8. }

Comme toujours, tout développement doit comporter sa classe de test. Mais ici, elle est très simple, vous allez le voir.

Nous faisons appel à la méthode System.schedule() qui attend trois paramètres :

- un nom pour le job dans Salesforce.com ;

- la fréquence d'exécution du batch ;

- une instance de la classe qui exécute le batch.

Le tout, entre les balises Test.startTest() et Test.stopTest() bien entendu.

1. @isTest(seeAllData=false)

2. private class ScheduleBatchEventsAccountPointsCal_TEST {

4. static testMethod void ScheduleBatchSalesGrowthCalculation_TEST(){

5. Test.startTest();

7. System.schedule('ScheduleBatchEventsAccountPointsCalc', '0 0 3 * * ?', new ScheduleBatchEventsAccountPointsCalc());

9. Test.stopTest();

10. }

11. }

Il ne nous reste qu'une étape dans la planification d'un batch.

Cela se fait en une ligne de code à exécuter soit dans la console du développeur soit dans la section Exécution anonyme dans le plugin Force.com IDE sous Eclipse.

Nous exécutons la même méthode que celle utilisée dans notre classe de test vue précédemment System.schedule().

L'expression de la planification comprend six paramètres obligatoires et un optionnel :

- les secondes ;

- les minutes ;

- les heures ;

- le jour du mois ;

- le mois ;

- le jour de la semaine ;

- l'année.

Voilà les valeurs disponibles pour l'expression de la planification :

Je ne vais pas redétailler les trois paramètres qu'elle utilise. Dans mon cas, je souhaite que le batch s'exécute toutes les nuits à 3 h 00.

La ligne de code ressemble donc à ceci :

1. System.schedule('ScheduleBatchEventsAccountPointsCalc', '0 0 3 * * ?', new ScheduleBatchEventsAccountPointsCalc());

La fréquence d'exécution du batch (2e paramètre) correspond à l'expression CRON que l'on retrouve dans n'importe quel système Unix et Windows.

VII - Conclusion

Nous arrivons à la fin de ce tutoriel, j'espère vous avoir correctement expliqué comment développer, planifier et tester un batch dans Salesforce.com.

Cela peut paraître long et ennuyeux lorsque l'on développe dans Salesforce.com (développement + test), mais cela s'avère nécessaire pour s'assurer que ce que l'on fait fonctionne correctement parce qu'une fois qu'une donnée en production devient périmée suite à un faux comportement, cela est trop tard et ce sera toujours le développeur qui se fera taper sur les doigts, donc il est important de livrer quelque chose de sain.

Comme je suis quelqu'un de sympa, je vous mets à disposition (ici), toutes les sources qui ont été nécessaires à la réalisation de cet article, à savoir :

- le batch ;

- la classe de test du batch ;

- la classe d'exécution du batch ;

- la classe de test de la classe d'exécution du batch.

Elles sont également disponibles sur mon compte Bitbucket à cette adresse.

VIII - Ressources utiles

- Apex Batch Processing

- Using Batch Apex

- Apex Scheduler

- Understanding Execution Governors and Limits

- Tester son code Apex

IX - Remerciements

Je tiens à remercier tous ceux qui m'ont permis de réaliser cet article, je parle de :

- vermine;

- ClaudeLELOUP ;

- milkoseck.