' P '

whatever I will forget

Apex 一括処理

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 オブジェクトを返す。
  • 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にデータ挿入は不可。LastNameFirstNameをセットする
  • 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);
        }
    }
}