Salesforce学習記録-Apex非同期

# 非同期Apex機能のまとめ
[ここから](https://developer.salesforce.com/docs/atlas.ja-jp.224.0.apexcode.meta/apexcode/apex_async_overview.htm)の抜粋
| 非同期Apex機能 | 利用ケース |
|:-|:-|
|[Future](#futureのメソッド)   | 長時間を要するメソッドがあり、Apex トランザクションの遅延を防止する必要がある場合|
||外部 Web サービスへのコールアウトを実行する場合|
||DML 操作を分離して混合保存 DML エラーを回避する場合  |
|[Queueable](#キュー)   | 長時間を要する操作を開始し、その ID を取得する場合|
||複雑なデータ型をジョブに渡す場合|
||ジョブをチェーニングする場合  |
|[Batchable](#一括処理)|大量のデータを処理する長時間のジョブを複数バッチで実行する必要がある場合 (データベースメンテナンスジョブなど)|
||通常のトランザクションで許容されるよりも大きなクエリ結果が必要になるジョブの場合   |
|[Schedulable](#スケジューラ)|特定のスケジュールで実行するために Apex クラスをスケジュールする場合|

# futureのメソッド
## futureの制限
future メソッドを定義するには、future アノテーションを使用してアノテーションを付加します。
メソッドは静的メソッドである必要があり、void 型のみを返します。
外部サービスへのコールアウトを実行するは、コールアウトが許可されることを示す追加パラメータ (callout=true) を利用します。
future メソッドで別の future メソッドを呼び出すことはできません。

```Apex:
global class FutureMethodRecordProcessing
{
    @future(callout=true)
    public static void processRecords(List<ID> recordIds)
    {   
         // Get those records based on the IDs
         List<Account> accts = [SELECT Name FROM Account WHERE Id IN :recordIds];
         // Process records
    }
}
```

## future メソッドのテスト
future アノテーションのあるメソッドをテストするには、**startTest()、stopTest() コードブロック内**でメソッドを含むクラスをコールします。**startTest メソッドの後**に実行されたすべての**非同期コールはシステムによって収集**されます。**stopTest を実行する**場合、すべての**非同期プロセスが同期して実行**されます。

```Apex:
@isTest
private class MixedDMLFutureTest {
    @isTest static void test1() {
        User thisUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
       // System.runAs() allows mixed DML operations in test context
        System.runAs(thisUser) {
            // startTest/stopTest block to run future method synchronously
            Test.startTest();        
            MixedDMLFuture.useFutureMethod();
            Test.stopTest();
        }
    }
}
```

# キュー
## キューメリット
・**ジョブ ID** の取得: System.enqueueJob メソッドを呼び出してジョブを送信すると、メソッドは新しいジョブの ID を返します。この ID は AsyncApexJob レコードの ID に対応します。この ID を使用して、Salesforce ユーザインターフェースの **[Apex ジョブ] ページ**から、またはプログラムで **AsyncApexJob のレコードを照会**する方法で、ジョブを識別してその進行状況を**監視できます**。
・**非プリミティブ型の使用**: キュー可能クラスには、sObject 型やカスタム Apex 型など、非プリミティブデータ型のメンバー変数を含めることができます。
・ジョブの**チェーニング**: 実行中のジョブから 2 つ目のジョブを開始することで、2 つのジョブを連鎖的に実行することができます。ジョブのチェーニングは、別の先行プロセスに依存する処理を実行する必要がある場合に便利です。

```Apex:実装例
public class AsyncExecutionExample implements Queueable {
    public void execute(QueueableContext context) {
        Account a = new Account(Name='Acme',Phone='(415) 555-1212');
        insert a;        
    }
}

//ジョブとしてキューに追加するには、次のメソッドをコールします。
ID jobID = System.enqueueJob(new AsyncExecutionExample());

//AsyncApexJob に対する SOQL クエリを実行し、ジョブ進行状況を照会できます。
AsyncApexJob jobInfo = [SELECT Status,NumberOfErrors FROM AsyncApexJob WHERE Id=:jobID];
```
## キュー可能ジョブのテスト
キュー可能ジョブは、非同期プロセスです。
Test.startTest と Test.stopTest 間のブロック内でキューに送信する必要があります。システムは、テストメソッドで開始されたすべての非同期プロセスを、Test.stopTest ステートメントの後に同期して実行します。

```Apex:Test例
@isTest
public class AsyncExecutionExampleTest {
    static testmethod void test1() {
        // startTest/stopTest block to force async processes 
        //   to run in the test.
        Test.startTest();        
        System.enqueueJob(new AsyncExecutionExample());
        Test.stopTest();
    }
}
```

## ジョブのチェーニング
別のジョブで他の処理を実行した後にジョブを実行する必要がある場合、キュー可能ジョブをチェーニングできます。
ジョブを別のジョブにチェーニングするには、キュー可能クラスの **execute() メソッドから 2 つ目のジョブを送信**します。
チェーニングされたジョブの**深度に制限はありません**。つまり、1 つのジョブから別のジョブにチェーニングし、このプロセスを新しい子ジョブごとに**繰り返して新しい子ジョブにリンクできます**。
ジョブをチェーニングするとき、実行中のジョブから System.enqueueJob で追加できるジョブは 1 つのみです。
```Apex:実装例
public class AsyncExecutionExample implements Queueable {
    public void execute(QueueableContext context) {
        // Your processing logic here       

        // Chain this job to next job by submitting the next job
        System.enqueueJob(new SecondJob());
    }
}
```

# スケジューラ
特定の時間に実行されるように Apex クラスを呼び出すには、まずクラスに Schedulable インターフェースを実装し、
Salesforce ユーザインターフェースの [Apex をスケジュール] ページ
または System.schedule メソッドのいずれかを使用してスケジュールを指定します。

一度にスケジュールできる Apex ジョブの数は 100 です。
現在の数を確認するには、Salesforce の [スケジュール済みジョブ] ページを表示し、データ型の検索条件を **[スケジュール済み Apex] **にしてカスタムビューを作成します。
また、**CronTrigger オブジェクトおよび CronJobDetail オブジェクトをプログラムで照会する**ことで、Apex スケジュール済みジョブの数を取得することもできます。

System.Schedule メソッドは、ジョブの名前、ジョブの実行予定日時を表すために使用する式
 Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
 設定詳細は [ここ](https://developer.salesforce.com/docs/atlas.ja-jp.224.0.apexcode.meta/apexcode/apex_scheduler.htm) を参照

```Apex:実装例
global class scheduledMerge implements Schedulable {
   global void execute(SchedulableContext SC) {
      mergeNumbers M = new mergeNumbers(); 
   }
}

//実装よりスケジュール設定
scheduledMerge m = new scheduledMerge();
String sch = '20 30 8 10 2 ?';
String jobID = system.schedule('Merge Job', sch, m);

//クエリを使用したスケジュール済みジョブの進行状況の追跡方法１
CronTrigger ct = 
    [SELECT TimesTriggered, NextFireTime
    FROM CronTrigger WHERE Id = :jobID];

//クエリを使用したスケジュール済みジョブの進行状況の追跡方法2
// SchedulableContext 引数の変数に対して getTriggerId をコールすることで現在のジョブの ID を取得できます。
CronTrigger ct = 
    [SELECT TimesTriggered, NextFireTime
    FROM CronTrigger WHERE Id = :SC.getTriggerId()];
```
## Apex スケジューラのテスト

スケジュールされた Apex をテストするとき、結果に対してテストする前にスケジュール済みジョブが終了している必要があります。System.schedule メソッドを実行する前後で、テストメソッド startTest と stopTest を使用して、テストを続行する前にスケジュール済みジョブが終了するようにします。

```Apex:テスト用クラス
global class TestScheduledApexFromTestMethod implements Schedulable {

// This test runs a scheduled job at midnight Sept. 3rd. 2022

   public static String CRON_EXP = '0 0 0 3 9 ? 2022';
   
   global void execute(SchedulableContext ctx) {
      CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
                FROM CronTrigger WHERE Id = :ctx.getTriggerId()];

      System.assertEquals(CRON_EXP, ct.CronExpression);
      System.assertEquals(0, ct.TimesTriggered);
      System.assertEquals('2022-09-03 00:00:00', String.valueOf(ct.NextFireTime));

      Account a = [SELECT Id, Name FROM Account WHERE Name = 
                  'testScheduledApexFromTestMethod'];
      a.name = 'testScheduledApexFromTestMethodUpdated';
      update a;
   }   
}
```

```Apex:テストソース
@istest
class TestClass {

   static testmethod void test() {
   Test.startTest();

      Account a = new Account();
      a.Name = 'testScheduledApexFromTestMethod';
      insert a;

      // Schedule the test job

      String jobId = System.schedule('testBasicScheduledApex',
      TestScheduledApexFromTestMethod.CRON_EXP, 
         new TestScheduledApexFromTestMethod());

      // Get the information from the CronTrigger API object
      CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
         NextFireTime
         FROM CronTrigger WHERE id = :jobId];

      // Verify the expressions are the same
      System.assertEquals(TestScheduledApexFromTestMethod.CRON_EXP, 
         ct.CronExpression);

      // Verify the job has not run
      System.assertEquals(0, ct.TimesTriggered);

      // Verify the next time the job will run
      System.assertEquals('2022-09-03 00:00:00', 
         String.valueOf(ct.NextFireTime));
      System.assertNotEquals('testScheduledApexFromTestMethodUpdated',
         [SELECT id, name FROM account WHERE id = :a.id].name);

   Test.stopTest();

   System.assertEquals('testScheduledApexFromTestMethodUpdated',
   [SELECT Id, Name FROM Account WHERE Id = :a.Id].Name);
   }
}
```
# 一括処理
[ここから](https://developer.salesforce.com/docs/atlas.ja-jp.224.0.apexcode.meta/apexcode/apex_batch.htm)の抜粋
 ・Apex の一括処理を使用して、長時間にわたり実行される複雑なプロセスを構築できるようになりました。
 ・Apex 一括処理は、レコードの小さいバッチに対して動作し、レコードセット全体を管理しやすい**チャンクに分割して処理**します。
 ・Apex の一括処理は、インターフェースとして公開され、開発者によって実行される必要があります。一括処理ジョブは実行時に Apex を使用してプログラムで起動できます。
　・**一度に実行できる**キュー内または有効な**一括処理ジョブは 5 件のみ**です。Salesforce の **[スケジュール済みジョブ] ページ**を表示するか、プログラムで SOAP API を使用して **AsyncApexJob オブジェクトを照会**することで、現在のジョブ件数を確認できます。
　・Apex の一括処理ジョブが実行されると、一括処理ジョブを送信したユーザにメール通知が送信されます。
　・Apex の一括処理が呼び出されるたびに AsyncApexJob レコードが作成されます。ジョブの状況、エラーの数、進行状況、送信者を取得する SOQL クエリを構成するには、AsyncApexJob レコードの ID を使用します。
　・クラス内のすべてのメソッドは global または public として定義する必要があります。
　
## Database.Batchable インターフェースの実装

### start メソッド
インターフェースメソッド **execute に渡すレコードまたはオブジェクトを収集**するには、
Apex 一括処理ジョブの冒頭でstart メソッドをコールします。
このメソッドは、**Database.QueryLocator** オブジェクト、
またはジョブに渡すレコードやオブジェクトが含まれる **Iterable オブジェクト**を返します。
```Apex:start メソッド
global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {}
```

### execute メソッド
**データの処理単位ごとに必要な処理を実行する**には、execute メソッドを使用します。
このメソッドは、メソッドに渡す**レコードのバッチごとにコールされます**。
このメソッドは次を取得します。
 Database.BatchableContext オブジェクトへの参照。
 List<sObject> などの sObjects のリストまたはパラメータ化された型のリスト。
 Database.QueryLocator を使用している場合は、返されたリストを使用します。

```Apex:execute メソッド
global void execute(Database.BatchableContext BC, list<P>){}
```

### finish メソッド
確認メールの送信や後処理操作を行う場合に、finish メソッドを使用します。
このメソッドは、**すべてのバッチが処理された後にコールされます**。

```Apex:finish メソッド
global void finish(Database.BatchableContext BC){}
```
### Apex の一括処理の例
```Apex:Database.QueryLocator を使用
//バッチクラス
global class UpdateAccountFields implements Database.Batchable<sObject>{
   global final String Query;
   global final String Entity;
   global final String Field;
   global final String Value;

   global UpdateAccountFields(String q, String e, String f, String v){
             Query=q; Entity=e; Field=f;Value=v;
   }
   global Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }
   global void execute(Database.BatchableContext BC, 
                       List<sObject> scope){
      for(Sobject s : scope){s.put(Field,Value); 
      }      update scope;
   }
   global void finish(Database.BatchableContext BC){
   }
}

//バッチ呼び出し
//削除されてまだごみ箱に入っている取引先や請求書を除外するには、この変更したサンプルのように、
//SOQL クエリの WHERE 句に isDeleted=false を付加
String q = 'SELECT Industry FROM Account WHERE isDeleted=false LIMIT 10';
String e = 'Account';
String f = 'Industry';
String v = 'Consulting';
//5のパラメータはexecute メソッドに渡すレコードの数、省略可能。MAX2000　デフォルトは 200
Id batchInstanceId = Database.executeBatch(new UpdateAccountFields(q,e,f,v), 5);
```
### Apex の一括処理でのコールアウトの使用
```Apex:Database.AllowsCallouts
global class SearchAndReplace implements Database.Batchable<sObject>, 
   Database.AllowsCallouts{
}
```
### Apex の一括処理での状態の使用
クラス定義で Database.Stateful を指定すると、これらのトランザクション間で状態を保持できます。Database.Stateful を使用するとき、インスタンスメンバー変数のみがトランザクション間で値を保持します。静的メンバー変数は、トランザクション間で値を保持せず、リセットされます。
```Apex:Database.Stateful レコードが処理されるとカスタム項目 total__c が集計されます。
global class SummarizeAccountTotal implements 
    Database.Batchable<sObject>, Database.Stateful{

   global final String Query;
   global integer Summary;
  
   global SummarizeAccountTotal(String q){Query=q;
     Summary = 0;
   }
   global Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }   
   global void execute(
                Database.BatchableContext BC, 
                List<sObject> scope){
      for(sObject s : scope){
         Summary = Integer.valueOf(s.get('total__c'))+Summary;
      }
   }
global void finish(Database.BatchableContext BC){
   }
}
```

### Apex の一括処理のテスト
executeBatch メソッドは、匿名プロセスを開始します。
テストメソッド startTest と stopTest を executeBatch メソッドの前後に使用して、非同期で処理された一括処理ジョブが完了してからテストを続行するようにします。startTest メソッドの後に実行されたすべての非同期コールはシステムによって収集されます。

```Apex:Database.AllowsCallouts
public static testMethod void testBatch() {
   user u = [SELECT ID, UserName FROM User 
             WHERE username='testuser1@acme.com'];
   user u2 = [SELECT ID, UserName FROM User 
              WHERE username='testuser2@acme.com'];
   String u2id = u2.id;
// Create 200 test accounts - this simulates one execute.  
// Important - the Salesforce.com test framework only allows you to 
// test one execute.  

   List <Account> accns = new List<Account>();
      for(integer i = 0; i<200; i++){
         Account a = new Account(Name='testAccount'+'i', 
                     Ownerid = u.ID); 
         accns.add(a);
      }
   
   insert accns;
   //Start
   Test.StartTest();
   OwnerReassignment reassign = new OwnerReassignment();
   reassign.query='SELECT ID, Name, Ownerid ' +
            'FROM Account ' +
            'WHERE OwnerId=\'' + u.Id + '\'' +
            ' LIMIT 200';
   reassign.email='admin@acme.com';
   reassign.fromUserId = u.Id;
   reassign.toUserId = u2.Id;
   //バッチ実行
   ID batchprocessid = Database.executeBatch(reassign);
   //Stop
   Test.StopTest();

   //実行結果確認
   System.AssertEquals(
           database.countquery('SELECT COUNT()'
              +' FROM Account WHERE OwnerId=\'' + u2.Id + '\''),
           200);  
   
   }
}
```