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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
global class
BatchEventsAccountPointsCalculation implements
Database.Batchable<
sObject>
, Database.Stateful{
public
String query;
global Database.QueryLocator start
(
Database.BatchableContext bc){
return
Database.getQueryLocator
(
query);
}
global void
execute
(
Database.BatchableContext BC, List<
sObject>
scope){
}
global void
finish
(
Database.BatchableContext BC){
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
/** Batch qui calcule les points de participation à des événements commerciaux des comptes **/
global class
BatchEventsAccountPointsCalculation implements
Database.Batchable<
sObject>
, Database.Stateful{
public
String query;
private
Integer currentYear;
private
List<
Account>
accountList;
private
String silverLabel =
System.Label.Label_silver;
private
String goldLabel =
System.Label.Label_gold;
private
String platiniumLabel =
System.Label.Label_platinium;
global Database.QueryLocator start
(
Database.BatchableContext bc){
this
.accountList =
new
List<
Account>(
);
this
.currentYear =
Date.today
(
).year
(
);
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'
;
return
Database.getQueryLocator
(
this
.query);
}
global void
execute
(
Database.BatchableContext BC, List<
sObject>
scope){
// Pour chaque compte
for
(
sObject aScope : scope){
Account anAccount =
(
Account) aScope;
Decimal nbPoints =
0
;
// Pour chaque participation du compte
for
(
Participation__c aParticipation : anAccount.Participations__r){
// Si compte silver
if
(
anAccount.Level__c.equals
(
silverLabel) &&
aParticipation.Events_Business_Points__r.Silver_level_points__c !=
null
){
nbPoints +=
aParticipation.Events_Business_Points__r.Silver_level_points__c;
}
// Sinon si compte gold
else
if
(
anAccount.Level__c.equals
(
goldLabel) &&
aParticipation.Events_Business_Points__r.Gold_level_points__c !=
null
){
nbPoints +=
aParticipation.Events_Business_Points__r.Gold_level_points__c;
}
// Sinon si compte platinium
else
if
(
anAccount.Level__c.equals
(
platiniumLabel) &&
aParticipation.Events_Business_Points__r.Platinium_level_points__c !=
null
){
nbPoints +=
aParticipation.Events_Business_Points__r.Platinium_level_points__c;
}
}
anAccount.Points__c =
nbPoints;
this
.accountList.add
(
anAccount);
}
// Mise a jour des comptes
update this
.accountList;
}
global void
finish
(
Database.BatchableContext BC){
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
/** Classe de test du batch « BatchEventsAccountPointsCalculation » **/
@isTest
private
class
BatchEventsAccountPointsCalculation_TEST {
static
List<
Account>
accountList;
static
List<
Business_Event__c>
businessEventList;
static
List<
Events_Business_Points__c>
eventBusinessPointList;
static
List<
Participation__c>
participationList;
static
String silverLabel =
System.Label.Label_silver;
static
String goldLabel =
System.Label.Label_gold;
static
String platiniumLabel =
System.Label.Label_platinium;
static
Date todayDate;
static
void
init
(
){
todayDate =
Date.today
(
);
/** Account **/
accountList =
new
List<
Account>(
);
accountList.add
(
new
Account
(
Name =
'Test name silver'
,
Level__c =
silverLabel
));
accountList.add
(
new
Account
(
Name =
'Test name gold'
,
Level__c =
goldLabel
));
accountList.add
(
new
Account
(
Name =
'Test name platinium'
,
Level__c =
platiniumLabel
));
insert accountList;
/** Business_Event__c **/
businessEventList =
new
List<
Business_Event__c>(
);
businessEventList.add
(
new
Business_Event__c
(
Name =
'Salesforce1 World Tour Paris'
,
Place__c =
'Paris'
));
businessEventList.add
(
new
Business_Event__c
(
Name =
'Dreamforce'
,
Place__c =
'Los Angeles'
));
businessEventList.add
(
new
Business_Event__c
(
Name =
'Salesforce1 World Tour London'
,
Place__c =
'London'
));
insert businessEventList;
/** Events_Business_Points__c **/
eventBusinessPointList =
new
List<
Events_Business_Points__c>(
);
eventBusinessPointList.add
(
new
Events_Business_Points__c
(
Business_Event__c =
businessEventList[0
].Id,
Year__c =
todayDate.year
(
)-
1
,
Silver_level_points__c =
10
,
Gold_level_points__c =
15
,
Platinium_level_points__c =
30
));
eventBusinessPointList.add
(
new
Events_Business_Points__c
(
Business_Event__c =
businessEventList[0
].Id,
Year__c =
todayDate.year
(
),
Silver_level_points__c =
20
,
Gold_level_points__c =
35
,
Platinium_level_points__c =
40
));
eventBusinessPointList.add
(
new
Events_Business_Points__c
(
Business_Event__c =
businessEventList[1
].Id,
Year__c =
todayDate.year
(
)-
1
,
Silver_level_points__c =
100
,
Gold_level_points__c =
150
,
Platinium_level_points__c =
300
));
eventBusinessPointList.add
(
new
Events_Business_Points__c
(
Business_Event__c =
businessEventList[1
].Id,
Year__c =
todayDate.year
(
),
Silver_level_points__c =
150
,
Gold_level_points__c =
300
,
Platinium_level_points__c =
450
));
eventBusinessPointList.add
(
new
Events_Business_Points__c
(
Business_Event__c =
businessEventList[2
].Id,
Year__c =
todayDate.year
(
),
Silver_level_points__c =
120
,
Gold_level_points__c =
270
,
Platinium_level_points__c =
430
));
insert eventBusinessPointList;
/** Participation__c **/
participationList =
new
List<
Participation__c>(
);
participationList.add
(
new
Participation__c
(
Account__c =
accountList[0
].Id,
Events_Business_Points__c =
eventBusinessPointList[0
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[0
].Id,
Events_Business_Points__c =
eventBusinessPointList[1
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[1
].Id,
Events_Business_Points__c =
eventBusinessPointList[4
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[1
].Id,
Events_Business_Points__c =
eventBusinessPointList[0
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[2
].Id,
Events_Business_Points__c =
eventBusinessPointList[1
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[2
].Id,
Events_Business_Points__c =
eventBusinessPointList[2
].Id
));
participationList.add
(
new
Participation__c
(
Account__c =
accountList[2
].Id,
Events_Business_Points__c =
eventBusinessPointList[4
].Id
));
insert participationList;
}
/** Teste les points du compte **/
static
testMethod void
testAccountPoint
(
) {
init
(
);
Test.startTest
(
);
Database.executeBatch
(
new
BatchEventsAccountPointsCalculation
(
));
Test.stopTest
(
);
// Récupération des comptes dans Salesforce
List<
Account>
newAccountList =
[
SELECT Id, Name, Points__c
FROM Account
WHERE Id IN :accountList
];
// Verification des points
System.assertEquals
(
eventBusinessPointList[1
].Silver_level_points__c, newAccountList[0
].Points__c);
System.assertEquals
(
eventBusinessPointList[4
].Gold_level_points__c, newAccountList[1
].Points__c);
System.assertEquals
((
eventBusinessPointList[1
].Platinium_level_points__c +
eventBusinessPointList[4
].Platinium_level_points__c), newAccountList[2
].Points__c);
}
}
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.
2.
3.
4.
5.
6.
7.
8.
global class
ScheduleBatchEventsAccountPointsCalc implements
Schedulable{
global void
execute
(
SchedulableContext ctx) {
Database.executeBatch
(
new
BatchEventsAccountPointsCalculation
(
));
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@isTest
(
seeAllData=
false
)
private
class
ScheduleBatchEventsAccountPointsCal_TEST {
static
testMethod void
ScheduleBatchSalesGrowthCalculation_TEST
(
){
Test.startTest
(
);
System.schedule
(
'ScheduleBatchEventsAccountPointsCalc'
, '0 0 3 * * ?'
, new
ScheduleBatchEventsAccountPointsCalc
(
));
Test.stopTest
(
);
}
}
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 :
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▲
IX. Remerciements▲
Je tiens à remercier tous ceux qui m'ont permis de réaliser cet article, je parle de :