とあるSalesforce親方の日記

ちょっと長いことSalesforce開発やってます。

チームスピリット Advent Calendar 2021:いまさら聞けない非同期Apexのはなし

 

この記事は、チームスピリット Advent Calendar 2021 1日目 の記事です。(公開日は気にするな!)

adventar.org

 

ごあいさつ

皆様こんにちは。相変わらずの親方です。

最近よく弄る機会があるので、Apex処理の「非同期Apex」について、思いついたTipsを書いてみました。

なにかとガバナ制限に縛られるApex開発において、トランザクションの壁を越えられる非同期処理は大量データの処理に不可欠です。「最近、Apexの変なエラーが止まらないの・・・」という方は必読です。

 

リファレンスはこちら

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_async_overview.htm

 

非同期Apexの特徴

メリット

  • ガバナ制限が緩和される
  • 大量データ処理ができる
  • スケジュール起動ができる
  • ポーリング的な処理も組める
  • 非同期でしかアクセスできないオブジェクトもある

デメリット

非同期Apexの一覧
Apex では、複数の方法で Apex コードを非同期に実行できます。ニーズに最も合う非同期 Apex 機能を選択してください。

次の表は、非同期 Apex 機能とそれぞれを使用するケースの一覧です。

@future メソッド 
  • 長時間を要するメソッドがあり、Apex トランザクションの遅延を防止する必要がある場合
  • 外部 Web サービスへのコールアウトを実行する場合
  • DML 操作を分離して混合保存 DML エラーを回避する場合
キュー可能 Apex (Queueable)
  • 長時間を要する操作を開始し、その ID を取得する場合
  • 複雑なデータ型をジョブに渡す場合
  • ジョブをチェーニングする場合

Apexバッチ(Bachable)

  • 大量のデータを処理する長時間のジョブを複数バッチで実行する必要がある場合 (データベースメンテナンスジョブなど)
  • 通常のトランザクションで許容されるよりも大きなクエリ結果が必要になるジョブの場合

Apexスケジューラ(Schedulable)

  • 特定のスケジュールで実行するために Apex クラスをスケジュールする場合

1.@futureメソッド

もっとも簡単に実装できる非同期Apexです。staticなメソッドに"@future"アノテーションをつけるだけ!シンプルだけどガバナ制限を緩和するには十分使えます。

失敗した場合にハンドリングがきかないので、コケても構わない処理をやりましょう。

同期/非同期の両方で使えるメソッドを作る

とはいえコケたら困るのでちゃんとロールバックできる形で実行したい・・・そんなときもあると思います。以下のように実装すれば、呼び出し方の違いで簡単に同期/非同期を切り替えられます。

@future //非同期で実行する時はこちら
public static void sampleMethodFuture(){
    sampleMethod();
}
    
// 同期で良い時はこちら
public static void sampleMethod(){
    
    // 処理を書く
}

2.キュー可能Apex

Queueableインターフェースを実装することで使える、上の@futureよりしっかり使えるクラスです。具体的には

  • ジョブIDを返すため、後からSOQLで実行成否を確認できる
  • コケた時にリカバリ処理を実装できる:Transaction Finalizersを使う
  • メンバー変数を定義できるので、無制限なデータ型の項目を事前に引き渡せる
  • 非同期Apex処理を更に1つ呼べるので、それを繰り返すことで常駐処理が組める(使いすぎに注意!)
複数のApexトリガから呼べる汎用非同期ハンドラ

Apexトリガのハンドラを非同期化することで、レコード保存の待機時間を減らすことができます。それだけなら@futureでもできるのですが、引数をSObjectにすることで処理を汎用化できます。

 

非同期Apexクラス

public class multipleTriggerHandler implements Queueable {

    List<SObject> newList;      // Trigger.new
    Map<Id, SObject> oldMap;    // trigger.oldMap

    // コンストラクタでTriggerから対象データを取得
    public AtkChangeLogHandler(List<SObject> newList, Map<Id, SObject> oldMap) {
        this.newList = newList;
        this.oldMap = oldMap;
    }

    // ここが本命
    public void execute(QueueableContext context) {

        // 処理を書く
    }
}

呼び出し元のトリガ

trigger AccountQueueableTrigger on Account (after insert, after update) {

    // 非同期ハンドラ(TriggerのNew、Oldの内容を引き渡す)
    multipleTriggerHandler handler = new multipleTriggerHandler(Trigger.new, Trigger.oldMap);

    // 非同期処理を実行
    System.enqueueJob(handler);
}

3.Apexバッチ

言わずと知れた、5000万件の大量処理ができる神のような機能です。ListもしくはSOQLの検索結果を引数として、その件数分だけの非同期処理ができます。夜間に定期実行させているところも多いのでは?

実はバッチサイズを変えられる

ApexバッチはDatabase.executeBatch()で起動しますた、実はこの第二引数にバッチサイズを入れられます。これにより最小1~2000件単位(クエリから実行する場合、リストを渡す場合はそれ以上可)まで処理件数を変えられます。一部処理でガバナ制限エラーが発生したときは件数を減らしてみましょう。

Apexバッチ

public with sharing class AtkBatchClass implements Database.Batchable<sObject> {

    // ここでバッチ処理前のデータ整形をする
    public Database.QueryLocator start(Database.BatchableContext bc){

        // このSOQLの実行結果を使う
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    }

    // この部分が複数分割され並列実行される
    public void execute(Database.BatchableContext BC, List<Account> scope){
        
        // 一定件数ごとに分割されたレコードがscopeに入ってくる
        for(Account acc: scope)
            System.debug(acc.Name);
        }
    
    // バッチ処理終了後の結果まとめ、エラーハンドリング用
    public void finish(Database.BatchableContext BC){

        // 処理を書く(別にしなくても良い)
    }
}

バッチの呼び出し

    // デフォルトでは200件
    Database.executeBatch(batch);

    // 最小1件
    Database.executeBatch(batch, 1);

    // 2000件以上も可
    Database.executeBatch(batch, 2000);
実はそのままでスケジューリングできる

バッチを定時起動するために口述のスケジュール起動と組み合わせている実装も多いと思います。ですが実はApexバッチはSchedulableでなくともスケジュールする方法があるんです。System.scheduleBatch()を使います。

時刻指定はcronではなく○○分後、1回のみ起動します。

    // 起動タイミングは○○分後と指定
    System.scheduleBatch(batch, 'スケジュール起動', 60);

    // バッチサイズ指定もできる
    System.scheduleBatch(batch, 'スケジュール起動', 1440, scopeSize);

4.Apexスケジューラ

標準のスケジューラ、もしくはcron表記で定時実行ジョブを登録できます。普通にやると単独の非同期トランザクションになりますが、バッチの起動トリガにも使えます。

Apexバッチをスケジュール可能にする

もはやこれが通常運用なのかもしれないという程メジャーな運用ですが、BachableとSchedulableクラスがそれぞれ有るとかさばるので纏めちゃいましょう。ちょっと運用しやすくなります。

直接スケジューラ登録可能なApexバッチ

public with sharing class AtkBatchSchedulable implements Database.Batchable<sObject>, Schedulable {

    // スケジューラから起動した時の処理
    public void execute(SchedulableContext sc) {

        // ここで自分自身を呼ぶ
        AtkBatchSchedulable batch = new AtkBatchSchedulable();
        Database.executeBatch(batch);
    }

    // 以下通常のバッチ処理
    public Database.QueryLocator start(Database.BatchableContext bc){
    }

    public void execute(Database.BatchableContext BC, List<AtkEmpType__c> scope){
    }

    public void finish(Database.BatchableContext BC){
    }
}

おわりに

あんまり日本語で情報出てなさそうなTipsを探してみました。普段当たり前にやってるけど実は他の人が知らないことって意外とあると思います。見えるところに書いておくだけでも後々いいことありそうですね。

 

それではまた!