Apex 一括処理の使用
学習の目的
この単元を完了すると、次のことを理解できるようになります。
- Apex 一括処理を使用する場合。
- 一括処理使用時の Apex 制限の引き上げ。
- Apex 一括処理構文。
- Apex 一括処理のベストプラクティス。
一緒にトレイルを進みましょう
インストラクターと一緒にこの手順を進めますか? 次の動画をご覧ください。これは Trailhead Live の「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 31:26 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
Apex の一括処理
Apex 一括処理は、通常の処理制限を超える可能性がある大規模ジョブ (たとえば、レコード数が数千~数百万件ある場合など) の実行に使用します。Apex 一括処理を使用すると、プラットフォームの制限を超えずにレコードをバッチ単位で非同期に一括処理できます (「Apex 一括処理」と呼ばれるのはそのためです)。データの整理やアーカイブなど、処理するレコードの数が多い場合、Apex 一括処理が適しています。
Apex 一括処理の内部的なしくみは次のとおりです。Apex 一括処理を使用して 100 万件のレコードを処理するとします。一括処理クラスの実行ロジックは、処理するレコードのバッチごとに 1 回コールされます。一括処理クラスを呼び出すたびに、ジョブは Apex ジョブキューに置かれ、別個のトランザクションとして実行されます。この機能には 2 つの大きな利点があります。
- 各トランザクションは新しいガバナ制限のセットで開始されるため、コードをガバナ実行制限内に抑えやすくなります。
- いずれかのバッチが処理に失敗しても、他のすべての正常に処理された一括処理トランザクションはロールバックされません。
Apex 一括処理構文
Apex 一括処理クラスを記述するには、クラスに Database.Batchable
インターフェースを実装して、次の 3 つのメソッドを含める必要があります。
start
インターフェースメソッド execute に渡して処理するレコードまたはオブジェクトを収集するために使用されます。このメソッドは、Apex 一括処理ジョブの開始時に 1 回コールされ、Database.QueryLocator
オブジェクトか、ジョブに渡すレコードやオブジェクトが含まれる Iterable
オブジェクトを返します。
ほとんどの場合、QueryLocator
は、単純な SOQL クエリを使用して一括処理ジョブのオブジェクトの範囲を生成します。ただし、API コールの結果のループ処理や、execute メソッドに渡す前のレコードの前処理など、複雑な処理が必要な場合は、「リソース」セクションの「カスタムイテレーター」のリンク先を参照してください。
QueryLocator
オブジェクトを使用する場合、SOQL クエリによって取得されるレコード合計数に対するガバナ制限は無視され、最大 5,000 万レコードまで照会できます。ただし、Iterable
オブジェクトを使用する場合、SOQL クエリによって取得されるレコード合計数に対するガバナ制限はそのまま適用されます。
execute
メソッドに渡されたデータの処理単位 (バッチ) ごとに実際の処理を実行します。デフォルトのバッチサイズは 200 レコードです。レコードのバッチが start メソッドから受け取った順序で実行される保証はありません。
このメソッドは次を取得します。
Database.BatchableContext
オブジェクトへの参照。List<sObject>
などの sObject のリストまたはパラメーター化された型のリスト。Database.QueryLocator
を使用している場合は、返されたリストを使用します。
finish
後処理操作 (メールの送信など) の実行に使用され、すべてのバッチが処理された後に 1 回コールされます。
Apex 一括処理クラスのスケルトンは次のようになります。
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 } }
一括処理クラスの呼び出し
一括処理クラスを呼び出すには、一括処理クラスをインスタンス化し、そのインスタンスを使用して Database.executeBatch
をコールするだけです。
MyBatchClass myBatchObject = new MyBatchClass(); Id batchId = Database.executeBatch(myBatchObject);
必要に応じて 2 つ目の scope パラメーターを渡し、バッチごとに execute メソッドに渡すレコードの数を指定することもできます。プロのヒント: ガバナ制限に近づいている場合は、このバッチサイズの制限が必要になることがあります。
Id batchId = Database.executeBatch(myBatchObject, 100);
Apex 一括処理を呼び出すごとに、AsyncApexJob
レコードが作成されるため、ジョブの進行状況を追跡できます。SOQL を介した進行状況の表示や、Apex ジョブキュー内のジョブの管理ができます。ジョブキューについてはこの後すぐに取り上げます。
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];
Apex 一括処理での状態の使用
Apex 一括処理は通常ステートレスです。Apex 一括処理ジョブの各実行は、個別のトランザクションとみなされます。たとえば、1,000 件のレコードを含み、デフォルトのバッチサイズを使用する Apex 一括処理ジョブは、それぞれ 200 件のレコードを含む 5 つのトランザクションとみなされます。
クラス定義で Database.Stateful
を指定すると、すべてのトランザクション間で状態を保持できます。Database.Stateful
を使用するとき、インスタンスメンバー変数のみがトランザクション間で値を保持します。状態を保持すると、処理されているレコードを数えたり集計したりする場合に役立ちます。次の例では、一括処理ジョブで取引先責任者レコードを更新し、影響を受けたレコードの合計数を追跡して、通知メールにその情報を追加できるようにします。
Apex 一括処理のサンプルコード
Apex 一括処理クラスの記述方法がわかったので、実用的な例を見てみましょう。米国内の企業のすべての取引先責任者は、郵送先住所として親会社の請求先住所が設定されていなければならないと定めるビジネス要件があるとします。ところが、ユーザーは正しい住所なしで新規取引先責任者を入力しています。要件はわかっているはずです。そこで、この要件が必ず適用されるようにする Apex 一括処理クラスを記述します。
次のサンプルクラスでは、QueryLocator
を使用する start()
メソッドによって渡されたすべての取引先レコードを検出し、関連付けられた取引先責任者を取引先の郵送先住所で更新します。最後に、一括ジョブの結果と、Database.Stateful
を使用して追跡した状態に基づく更新済みレコード数を含むメールを送信します。
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); } }
このコードはかなり簡単ですが、実際にはもう少し難解になる場合があります。処理を詳細に説明します。
start()
メソッドは、execute()
メソッドが個々のバッチで処理するすべてのレコードのコレクションを提供します。SOQL クエリでDatabase.getQueryLocator
をコールし、処理するレコードのリストを返します。ここでは、単純に [国(請求先)] が「USA」の取引先レコードをすべて照会します。- 200 レコードずつの各バッチが
execute()
メソッドの第 2 パラメーターで渡されます。execute()
メソッドは、各取引先責任者の郵送先住所を取引先の請求先住所に設定し、recordsProcessed
を増分して処理されたレコード数を記録します。 - ジョブが完了すると、finish メソッドが
AsyncApexJob
オブジェクト (一括処理ジョブに関する情報がリストされるテーブル) に対するクエリを実行して、ジョブの状況、送信者のメールアドレス、およびその他の情報を取得します。その後で、ジョブ情報と更新された取引先責任者の数を含む通知メールをジョブ送信者に送信します。
Apex 一括処理のテスト
Apex の開発とテストは連携して進むため、ここでは上記の一括処理クラスのテスト方法を説明します。簡単に言うと、レコードを挿入し、Apex 一括処理クラスをコールし、レコードが正しい住所で適切に更新されたことを確認します。
@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']); } }
setup メソッドが、請求先市区郡が「New York」、請求先国が「USA」の 10 件の取引先レコードを挿入します。次に、取引先ごとに、関連付けられた取引先責任者レコードを作成します。このデータは、一括処理クラスで使用されます。
test()
メソッドで、UpdateContactAddresses
一括処理クラスがインスタンス化され、Database.executeBatch
をコールして一括処理クラスのインスタンスを渡すことで呼び出されます。
Database.executeBatch
へのコールは Test.startTest
と Test.stopTest
ブロック内に含まれます。重要な処理はすべてここで行われます。ジョブは、Test.stopTest
へのコールの後に実行されます。Test.startTest
と Test.stopTest
の間の非同期コードは Test.stopTest
の後に同期して実行されます。
テストでは最後に、郵送先の市区郡が「New York」の取引先責任者レコードの数が挿入されたレコードの数 (10 件) と一致することを確認して、すべての取引先責任者が正しく更新されたことを検証しています。
ベストプラクティス
future メソッドと同様に、Apex 一括処理を使用するときにもいくつかの留意点があります。一括処理ジョブを高速に実行するには、Web サービスコールアウト回数を最小限にし、Apex 一括処理コードで使用されるクエリを調整します。キュー内のジョブ数が多い場合、一括処理ジョブの実行時間が長くなるほど、キューにある他のジョブが遅延する可能性が高くなります。ベストプラクティスとして、次のようなものがあります。
- レコードのバッチが複数ある場合は、必ず Apex 一括処理を使用します。複数のバッチを実行するにはレコード数が足りない場合は、Queueable Apex を使用することをお勧めします。
- できるだけ迅速にレコードを収集して実行できるようにすべての SOQL クエリを調整します。
- 遅延する可能性を最小限に抑えるには、作成する非同期要求の数を最小限に抑えます。
- 一括処理ジョブをトリガーから開始する場合は、細心の注意を払ってください。制限を超える数の一括処理ジョブをトリガーで追加しないようにする必要があります。
リソース
- Apex 開発者ガイド: Apex の一括処理
- Apex 開発者ガイド: Apex 一括処理の使用
- Apex 開発者ガイド: カスタムイテレーター
- Apex 開発者ガイド: TestSetup アノテーション