Apex 一括処理とは
- 通常の処理制限を超える可能性がある大規模ジョブ (たとえば、レコード数が数千~数百万件ある場合など) の実行に使用する
- 一括処理クラスの実行ロジックは、処理するレコードのバッチごとに 1 回コールされる
- 一括処理クラスを呼び出すたびに、ジョブは Apex ジョブキューに置かれ、別個のトランザクションとして実行される
- 利点は下記
- 通常はステートレスとなる。ジョブの各実行は通常のトランザクションと見做される
- クラス定義で
Database.Stateful
を指定すると、すべてのトランザクション間で状態を保持できる
- クラス定義で
一括処理の構文
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 } }
start
- インターフェースメソッド execute に渡して処理するレコードまたはオブジェクトを収集するために使用する
- 一括処理時に一回callされ、
Database.QueryLocator
オブジェクトか、ジョブに渡すレコードやオブジェクトが含まれる Iterable オブジェクトを返す。
- 一括処理時に一回callされ、
- インターフェースメソッド execute に渡して処理するレコードまたはオブジェクトを収集するために使用する
execute
- メソッドに渡されたデータの処理単位 (バッチ) ごとに実際の処理を実行する。
- デフォルトのバッチサイズは 200 レコード
- レコードのバッチが
start
メソッドから受け取った順序で実行される保証はない。
finish
- 後処理操作 (メールの送信など) の実行に使用され、すべてのバッチが処理された後に 1 回コールされる。
一括処理クラスの呼び出し
MyBatchClass myBatchObject = new MyBatchClass();
Id batchId = Database.executeBatch(myBatchObject);
executeBatch
メソッドにはexecute
メソッドに渡すレコードの数を指定することも可能- 上記を呼び出すごとに、
AsyncApexJob
レコードが作成されるため、ジョブの進行状況を下記のSOQLで確認可能
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];
サンプルコード
取引先責任者の郵送先住所を取引先の請求先住所に更新
バッチ処理の終了後に処理結果結果をメール送信(このメソッドは記載なし)
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); } }
テストクラスサンプルコード
- テストメソッドが実行できるバッチは 1 つのみ
@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']); } }
Challenge
Lead.Name
にデータ挿入は不可。LastName
、FirstName
をセットするCompany
にもデータをセットしないと怒られる
public class LeadProcessor implements Database.Batchable<sObject>{ public static Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator('SELECT Id FROM Lead'); } public void execute(Database.BatchableContext bc, List<Lead> scope) { for(Integer i=0; i<scope.size(); i++) { Lead l = scope.get(i); l.LeadSource = 'Dreamforce'; scope.set(i, l); } update scope; } public void finish(Database.BatchableContext bc) { AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email FROM AsyncApexJob WHERE Id = :bc.getJobId()]; system.debug(job); } }
@IsTest private class LeadProcessorTest { @TestSetup static void makeData(){ List<Lead> leads = new List<Lead>(); for(Integer i=0; i<200; i++) { leads.add(new Lead(FirstName='Taro', LastName='Test', Company='Test', LeadSource='test')); } insert leads; } @IsTest static void test() { Test.startTest(); LeadProcessor lp = new LeadProcessor(); Id batchId = Database.executeBatch(lp); Test.stopTest(); for(Lead l : [SELECT LeadSource FROM Lead]) { System.assertEquals('Dreamforce', l.LeadSource); } } }