场景描述:UAT过后,我们需要将客户的历史数据导进生产环境,由于记录体量很大,通常会先关闭部分Trigger,Process Builder,Workflow等来保证数据能成功导入,事后,为了保证业务数据的合理性,我们会使用Batch来更新那些被禁用后的逻辑。
Template Code:
Batch class:
global class ExampleBatchClass implements Database.Batchable{
global ExampleBatchClass(){
// Batch Constructor
}
// Start Method
global Database.QueryLocator start(Database.BatchableContext BC){
return Database.getQueryLocator(query);
}
// Execute Logic
global void execute(Database.BatchableContext BC, Listscope){
// Logic to be Executed batch wise
}
global void finish(Database.BatchableContext BC){
// Logic to be Executed at finish
}
}
Call the batch class:
ExampleBatchClass b = new ExampleBatchClass();
//Parameters of ExecuteBatch(context,BatchSize)
database.executebatch(b,10);
Note:
1. if batch size is not mentioned it is 200 by default.
2. 我们知道future方法不能在batch或者future方法中被调用,但是如果在trigger中调用future是可以的,前提是该trigger不能由batch触发。简言之,如果batch触发了trigger,而trigger调用了future不被允许,错误如下:
caused by: System.AsyncException: Future method cannot be called from a future or batch method
Sample:将Org中所有Lead历史数据的Status更新为Closed
Code Snap:
global class LeadProcessor implements Database.Batchable, Database.Stateful {
// instance member to retain state across transactions
global Integer recordsProcessed = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
// collect the batches of records or objects to be passed to execute
String str = 'select Id, Status from Lead';
return Database.getQueryLocator(str);
}
global void execute(Database.BatchableContext bc, List records){
// process each batch of records
List ldList = new List();
for(Lead l : records) {
l.Status = 'Closed';
ldList.add(l);
recordsProcessed ++;
}
update ldList;
}
global void finish(Database.BatchableContext bc){
// execute any post-processing operations
System.debug(recordsProcessed + ' records processed.');
AsyncApexJob job = [SELECT Id, Status, NumberOfErrors,
JobItemsProcessed,
TotalJobItems, CreatedBy.Email
FROM AsyncApexJob
WHERE Id = :bc.getJobId()];
}
}
finish方法在batch执行完毕执行,一般用作邮件通知,代码如下:
global void finish(Database.BatchableContext BC){
// Get the ID of the AsyncApexJob representing this batch job
// from Database.BatchableContext.
// Query the AsyncApexJob object to retrieve the current job's information.
AsyncApexJob a = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
TotalJobItems, CreatedBy.Email
FROM AsyncApexJob WHERE Id =
:BC.getJobId()];
// Send an email to the Apex job's submitter notifying of job completion.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {a.CreatedBy.Email};
mail.setToAddresses(toAddresses);
mail.setSubject('Apex Sharing Recalculation ' + a.Status);
mail.setPlainTextBody
('The batch Apex job processed ' + a.TotalJobItems +
' batches with '+ a.NumberOfErrors + ' failures.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
Test Code:
@isTest
private class LeadProcessorTest {
@testSetup
static void setup() {
List ldList = new List();
for(Integer i = 0; i < 200; i++) {
ldList.add(new Lead(LastName = 'Lead' + i, Company = 'com' + i, Status = 'Open'));
}
insert ldList;
}
static testMethod void test1() {
Test.startTest();
LeadProcessor lp = new LeadProcessor();
Id batchId = Database.executeBatch(lp);
Test.stopTest();
}
}
在Developer Console的Open Excute Anonymous Window执行以下代码:
LeadProcessor leadBach = new LeadProcessor();
Id batchId = Database.executeBatch(leadBach, 100);
参考资料:
https://trailhead.salesforce.com/modules/asynchronous_apex/units/async_apex_batch
补充:
Batch特性:如果我们在start里面写查询语句,如果查询中包含关系字段(除关系ID外),那么debug时是出不来关系字段的值的,这时候如果用公式字段代替关系字段,同样debug不出值;如果我们在excute里面做查询,那么debug出来的只有ID和Name的值,其他字段值也出不来。
举例:我们需要处理Account和Contact,在start里面查询语句为:select id, name, accountid, account.site from contact,那么只能debug出id, name, accountid的值;如果我们在excute里面写入:select id, name, site from account,只能debug出id和name的值。
建议:如果是跨对象的过滤,首先将子对象记录在start中查处来(Id),然后在excute里面将查出来的id作为参数传入外部异步方法,该方法主要用来使用select再次查询,这个时候就可以debug出关系字段了,意思是就可以做过滤了。
参考文档:http://www.iterativelogic.com/developing-custom-apex-rollup-summary-field-logic/
【纠偏与建议篇180802】:
早期偏见:batch查询跨对象(父查子,子查父),那么由于debug不出跨对象数据,那么自然无法使用此类值做逻辑判断。
下面代码两种组织思维(见注释/非注释部分),同样实现了将所有子记录的Name值以;隔开拼起来填到父记录某字段的需求。
global class BatchAccountLoginsPopulate implements Database.Batchable {
global Integer recordsProcessed = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
// String query = 'SELECT Id, Account__c FROM MT4_Accounts__c';
String query = 'SELECT Id, Name, Trading_Account_Login__c, (SELECT Id FROM MT4_Accounts__r) FROM Account WHERE Trading_Account_Login__c = NULL';
return Database.getQueryLocator(query);
}
// global void execute(Database.BatchableContext BC, list records) {
global void execute(Database.BatchableContext BC, list records) {
// List accList = new List();
// Set accIdSet = new Set();
// for(MT4_Accounts__c rAcc : records) {
// accIdSet.add(rAcc.Account__c);
// }
// if(accIdSet.size() > 0) {
// TradingAccountTriggerFunction.joinLoginToAccount(accIdSet);
// }
Set accIds = new Set();
for(Account acc : records) {
if(acc.MT4_Accounts__r.size() > 0) {
accIds.add(acc.Id);
}
}
if(accIds.size() > 0) {
TradingAccountTriggerFunction.joinLoginToAccount(accIds);
}
}
global void finish(Database.BatchableContext BC) {
AsyncApexJob a = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
TotalJobItems, CreatedBy.Email
FROM AsyncApexJob WHERE Id = :BC.getJobId()];
System.debug('[Apex Sharing Recalculation ' + a.Status + ']: The batch Apex job processed ' + a.TotalJobItems +
' batches with '+ a.NumberOfErrors + ' failures.');
}
}
写测试类可能遇到如下错误:原因是该字段字段类型为长文本字段
System.QueryException: field 'Trading_Account_Login__c' can not be filtered in a query call
【实践总结180802】:
写batch最佳实践是query部分最好以参数形式传进来,hard code会导致不灵活,如果部署到了生产环境刷数据如果有最新查询方案,动态传参会省大笔修改逻辑,写测试类,重新部署,修正错误等时间。
【Execute All Batch Classes180814】:
Are you creating schedule class for each batch class if you need to schedule that?
If yes, stop doing like that and start creating a universal (common scheduler class) like below and use it instead of creating multiple schedules.
Step 1:
Create a universal scheduler class
global class UniversalScheduler implements Schedulable {
global Database.Batchable batchClass{get;set;}
global Integer batchSize{get;set;} {batchSize = 200;}
global void execute(SchedulableContext sc) {
Database.executebatch(batchClass, batchSize);
}
}
Step 2:
Let say, You have two batch classes and you want to schedule, then schedule batch class like below using UniversalScheduler class
Batch Class 1:
AccountBatchProcess accBatch = new AccountBatchProcess(); // Batch Class Name
UniversalScheduler scheduler = new UniversalScheduler();
scheduler.batchClass = accBatch;
scheduler.batchSize = 100;
String sch = '0 45 0/1 1/1 * ? *';
System.schedule('Account Batch Process Scheduler', sch, scheduler);
Batch Class 2:
ContactBatchProcess cntBatch = new ContactBatchProcess(); // Batch Class Name
UniversalScheduler scheduler = new UniversalScheduler();
scheduler.batchClass = cntBatch;
scheduler.batchSize = 500;
String sch = '0 45 0/1 1/1 * ? *';
System.schedule('Contact Batch Process Scheduler', sch, scheduler);
【每隔5min执行一次Schedule方法】:
System.schedule('IMS_ScheduleCampMemberStatusUpdate 1', '0 00 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 2', '0 05 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 3', '0 10 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 4', '0 15 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 5', '0 20 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 6', '0 25 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 7', '0 30 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 8', '0 35 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 9', '0 40 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 10', '0 45 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 11', '0 50 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
System.schedule('IMS_ScheduleCampMemberStatusUpdate 12', '0 55 * * * ?', new IMS_ScheduleCampMemberStatusUpdate());
List JOBIDLIST = new List();
JOBIDLIST = [SELECT Id, CronJobDetail.name, NextFireTime, PreviousFireTime, State, StartTime, EndTime, CronExpression FROM CronTrigger where CronJobDetail.name like 'IMS_Schedule%'];
for(CronTrigger job:JOBIDLIST )
System.abortJob(job.id);
【其他话题】:
1. Using Aggregate SOQL queries/results in Batch Apex
2. Executing Batch Apex in Sequence