tubasa_gekituiのブログ

salesforce社の無料学習サイト「Trailhead」の覚え書きとか日記とか

【Spring ’20 の新機能】TransactionFinalizers機能を使用してみた【Let's Go 人柱】

この記事は Salesforce 開発者向けブログキャンペーンへのエントリー記事です。

本記事は、spring'20 の新機能 TransactionFinalizers を使用した非同期 Apex ジョブへのアクションの関連付け (パイロット)を使用してみた、です。

いろいろ機能がある中でなぜこの機能を選んだかと言いますと、
リリースノートを眺めていて「Finalizer」って単語を見た時に、 トリコロイダーと語呂が語が似ているな〜ってたまたま思っただけです、それだけです。それで決めました。
トリコロイダーって何?って方は下記リンクを参照してください。 www.nicovideo.jp

では、試していきます。

前提条件(spring' 20 時点)

spring' 20時点では TransactionFinalizers機能はスクラッチ組織でしか使用できません。
実際にdeveloper editionの組織にリリースノートのサンプルコード:AccountUpdateLoggingFinalizerを保存しようとすると以下のエラー(Type is not visible)が表示されます。

developer組織にFinalizerクラスを保存するとエラー

なので、試すにあたりスクラッチ環境を作成する必要があります。
「Dev Hub の有効化」「Salesforce CLI のインストール」からスクラッチ組織を作成するまでの一連の手順は以下のTrailheadプロジェクトが参考になります。
trailhead.salesforce.com ※. Git の手順や sfdx プロジェクト作成は無視して進めましょう。

「Dev Hub の有効化」「Salesforce CLI のインストール」ができているのであれば、以下のコマンドを順に実行します。【】の中は好きに決めてください。

sfdx force:auth:web:login -d -a 【devhub組織 alias名】
sfdx force:project:create -n 【sfdxプロジェクト名】
sfdx force:org:create -a 【scracth組織 alias名】 -s -f config/project-scratch-def.json

config/project-scratch-def.json(スクラッチ組織定義ファイル)の記載内容です。
f:id:tubasa_gekitui:20200316014506p:plain

ここで肝心なのは features に TransactionFinalizers を含めることです。
でないとスクラッチ組織で使用できません。何のために作成したのか?と小1時間考えることになります。 (小1時間は、筆者がリアルに原因究明のために考えた時間です。)

クラッチ組織定義ファイルの詳細については、下記リンクを参照して下さい。 developer.salesforce.com

今回お試しするユースケース

新機能の動作確認を最優先にするため、ユースケースはリリースノートの例よりシンプルにしています。

  • キュー可能 Apex内の処理では、全ての取引先に対しタスクを登録します。
    ※. 事前にスクラッチ組織には取引先を300件登録しておきます。
  • Finalizer内の処理では、タスクを追加した取引先名の一覧を
    自分のchatterフィードに投稿&ログオブジェクト(カスタムオブジェクト)に登録します。 ※. 事前にカスタムオブジェクトは作成しておきます。

以降に、非同期処理のコードは複数パターン設けていますが Finalizerクラスはすべて共通のコードです。

パターン1:キュー可能 Apexの処理が正常に終了する

まずは王道の正常に終了する、キュー可能 Apexの処理で上記にあげたユースケース内の処理を実行し、 TransactionFinalizers がどんな機能かを見ていきます。

コード

リリースノートに載っているサンプルを元にキュー可能Apex クラスとFinalizerクラスを用意します。

キュー可能Apex クラス

public class CreatingAccountTaskQueueable implements Queueable {
    
    public void execute(QueueableContext ctx) {
        try {
            System.Debug('キュー処理開始:Job ID:' + ctx.getJobId());
            
            // Create a transaction finalizer
            CreatingAccountTaskFinalizer finalizer = new CreatingAccountTaskFinalizer();
            
            // Attach the transaction finalizer to this queueable
            System.attachFinalizer(finalizer);
            
            // 取引先を取得し。タスクを登録
            List<Account> accounts = [select Id, name from Account];
            List<Task> tasks = new List<Task>();
            for(Account acc : accounts) {
                Task t = new Task(Subject = '取引先名_000に電話する', whatId = acc.Id, ActivityDate = Date.today().addDays(3));
                tasks.add(t);
                // 取引先情報を finalizerへ追加
                finalizer.addAcount(acc);
            }
            insert tasks;
            
        } catch(Exception e) {
            // 例外はそのままスロー
            System.Debug('Exception 発生:' + e.getStackTraceString());
            throw e;
        }
    }
}

Finalizerクラス

public class CreatingAccountTaskFinalizer implements Finalizer {

    List<String> accountNames;
    
    public CreatingAccountTaskFinalizer() {
        accountNames = new List<String>();
    }
    
    public void execute(FinalizerContext ctx) {        
        Id parentQueueableJobId = ctx.getAsyncApexJobId();
        System.Debug('Executing Finalizer that was attached to Queueable Job ID: ' + parentQueueableJobId);
        
        // キューのジョブ実行結果で内容が異なるメッセージ
        String msg = createMessage(ctx);
        System.Debug(msg);

        // 自分のフィードに投稿したりログオブジェクト(カスタムオブジェクト)に登録してみる。
        ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), UserInfo.getUserId(), ConnectApi.FeedElementType.FeedItem, msg);
        Logger__c log = new Logger__c(Message__c = msg);
        insert log;
    }

    private string createMessage(FinalizerContext ctx)  {
        String msg;
        if (ctx.getAsyncApexJobResult() == FinalizerParentJobResult.SUCCESS) {
            msg = '処理成功 Job ID : ' + ctx.getAsyncApexJobId() + '\n';
            msg += '取引先登録数 : ' + accountNames.size() + '\n';
            msg += '登録された取引先名 : ' + String.join(accountNames, ', ');
        } else {
            msg = '処理失敗 Job ID : ' + ctx.getAsyncApexJobId() + '\n';
            msg += 'メッセージ : ' + ctx.getAsyncApexJobException().getMessage() + '\n';
            msg += '取引先登録数 : ' + accountNames.size() + '\n';
            msg += '登録された取引先名 : ' + String.join(accountNames, ', ');
        }
        return msg;
    }

    public void addAcount(Account acc) {
        accountNames.add(acc.Name);
    }
}

実行結果

Anonymous Window から実行した結果です。
デバッグログより、キュー可能 Apex内の処理 → Finalizer の処理の順で実施されます。

正常系 キュー デバッグログ 正常系 Finalizer デバッグログ

ログインユーザのフィードやカスタムオブジェクトに Finalizerクラスの登録処理が反映されます。

正常系 Feed 正常系 ロガーオブジェクト

パターン2:例外(LimitExceptionでは無い)が発生する

リリースノートに記載されている内容が何とな~くわかったところでここからが本題です。
キュー可能 Apexの処理でガバナ制限違反では無い例外が発生した時に、TransactionFinalizers がどう機能するのか見ていきます。

コード

キュー可能 Apexの処理で例外を故意に発生させます。

public class CreatingAccountTaskQueueable2 implements Queueable {

    public void execute(QueueableContext ctx) {
        try {
            System.Debug('キュー処理開始:Job ID:' + ctx.getJobId());
            
            // Create a transaction finalizer
            CreatingAccountTaskFinalizer finalizer = new CreatingAccountTaskFinalizer();
            
            // Attach the transaction finalizer to this queueable
            System.attachFinalizer(finalizer);
            
            // 取引先を取得し、タスクを登録
            List<Account> accounts = [select Id, name from Account];
            List<Task> tasks = new List<Task>();
            for(Account acc : accounts) {
                Task t = new Task(Subject = '取引先名_000に電話する', whatId = acc.Id, ActivityDate = Date.today().addDays(3));
                tasks.add(t);
                // 取引先情報を finalizerへ追加
                finalizer.addAcount(acc);
            }
            update tasks; // 故意に例外(DMLException)を発生させます。
            
        } catch(Exception e) {
            // catch節のコードを通過したかどうかを確認したいだけなので、例外はそのままスロー
            System.Debug('Exception 発生:' + e.getStackTraceString());
            throw e;
        }
    }
}

実行結果

Anonymous Window から実行した結果です。
キュー可能 Apex内の処理で例外が発生し、Finalizer クラスに例外情報が引き継がれていることが分かります。

異常系1 デバッグログ キュー 異常系1 デバッグログ Finalizer

例外は発生しましたが、 Finalizerクラスの登録処理は反映されます。

異常系1 Feed 異常系1 ロガーオブジェクト

パターン3:例外:LimitException が発生する

キュー可能 Apexの処理でガバナ制限に違反をした時に、TransactionFinalizers がどう機能するのか見ていきます。

コード

キュー可能 Apexの処理で例外(LimitException)を故意に発生させます。

public class CreatingAccountTaskQueueable3 implements Queueable {

    public void execute(QueueableContext ctx) {
        try {
            System.Debug('キュー処理開始:Job ID:' + ctx.getJobId());
            
            // Create a transaction finalizer
            CreatingAccountTaskFinalizer finalizer = new CreatingAccountTaskFinalizer();
            
            // Attach the transaction finalizer to this queueable
            System.attachFinalizer(finalizer);
            
            // 取引先を取得し、タスクを登録
            List<Account> accounts = [select Id, name from Account];
            List<Task> tasks = new List<Task>();
            for(Account acc : accounts) {
                Task t = new Task(Subject = '取引先名_000に電話する', whatId = acc.Id, ActivityDate = Date.today().addDays(3));
                tasks.add(t);
                // 取引先情報を finalizerへ追加
                finalizer.addAcount(acc);

                insert t; // 故意に、ガバナ制限違反(LimitException)を発生させます。
            }
            
        } catch(Exception e) {
            // catch節のコードを通過したかどうかを確認したいだけなので、例外はそのままスロー
            System.Debug('Exception 発生:' + e.getStackTraceString());
            throw e;
        }
    }
}

実行結果

Anonymous Window から実行した結果です。
キュー可能 Apex内の処理でガバナ制限違反による例外が発生したのでcatch節は通過していません。
しかし、先のパターン同様にFinalizer クラスにガバナ制限違反による例外は引き継がれていることが分かります。

異常系2 デバッグログ キュー 異常系2 デバッグログ Finalizer

キュー可能 Apex内の処理でガバナ制限違反による例外が発生しましたが、Finalizer クラスの登録処理は問題なく行われています。
これよりガバナ制限のカウントは別ということが分かります。

異常系2 Feed 異常系2 ロガーオブジェクト

パターン4:キューではなくバッチで実装して実行してみる

パターン「キュー可能 Apexの処理が正常に終了」のコードをBatchableクラスに移植した時に、 TransactionFinalizers がどう機能するのか見ていきます。
「同じ非同期処理なのだから実は動くのでは?このツンデレめ!」というのを検証します。

コード

提示したユースケースのコードをBatchableクラスに移植してみました。

global class CreatingAccountTaskBatch implements Database.batchable<Account>{
    
    global Iterable<Account> start(Database.BatchableContext bc){ 
        System.Debug('バッチ start:Job ID:' + bc.getJobId());
        return [select Id, name from Account];
    }
    
    global void execute(Database.BatchableContext bc, List<Account> accounts){
        try {
            System.Debug('バッチ execute処理開始:Job ID:' + bc.getJobId());
            
            // Create a transaction finalizer
            CreatingAccountTaskFinalizer finalizer = new CreatingAccountTaskFinalizer();
            
            // Attach the transaction finalizer to this queueable
            System.attachFinalizer(finalizer);
            
            // 取引先を取得し、タスクを登録
            List<Task> tasks = new List<Task>();
            for(Account acc : accounts) {
                Task t = new Task(Subject = acc.Name + 'に電話する', whatId = acc.Id, ActivityDate = Date.today().addDays(3));
                tasks.add(t);
                // 取引先情報を finalizerへ追加
                finalizer.addAcount(acc);
            }
            insert tasks;
            
        } catch(Exception e) {
            // 例外はそのままスロー
            System.Debug('Exception 発生:' + e.getStackTraceString());
            throw e;
        }
    }
    
    global void finish(Database.BatchableContext bc){
        System.Debug('バッチ finish:Job ID:' + bc.getJobId());
    }
}

実行結果

Anonymous Window から実行した結果です。
メソッド start は正常終了します。(当然ですが バッチ start

メソッド execute のデバッグログより、System.attachFinalizer の行で例外「System.HandledException」が発生し処理が終了します。

バッチ execute バッチ execute 例外

メソッド finish は正常に実行されます。 バッチ finish

Finalizer に関するメソッドで処理が中断したため、 Finalizerクラスの登録処理は実行されません。

バッチ_Feed バッチ_ロガーオブジェクト

まとめ

  • キュー可能ジョブが例外がスローされずに正常終了した場合は、キュー可能ジョブの後にアタッチしたFinalizerクラスのコードが実行される。
  • キュー可能ジョブが例外(LimitException を含む)がスローして異常終了した場合でも、キュー可能ジョブの後にアタッチしたFinalizerクラスのコードが実行される。
  • バッチ処理にてFinalizerクラスをアタッチすると、処理実行時に System.attachFinalizer で例外「System.HandledException」(メッセージ:System.attachFinalizer(Finalizer) はこのコンテキストでは許可されません)がスローされる。

おまけ

現在絶賛稼働中の音楽ゲーム beatmania IIDX HEROIC VERSE より 冒頭で紹介した楽曲:錬成人間トリコロイダー の SP(Single Play) Another譜面 のプレーリザルトの写真です。

この譜面のラストの同時押しがきつくて悲しいかな、体力が持たないのでクリアできません (泣 最後ムリです、ラス殺し

譜面難易度は☆11 と表記されていますが、最後だけ☆12 だな・・・。

2020.03.22 追記
ウォーミングアップ直後&Assist Easyオプション付けてプレイしたらボーダークリア! Assist easy でボーダークリア(´;ω;&#x60;)ウゥゥ

やはりもう歳か・・・
最後はあんみつ使用したのでスコアが落ちてます。 自己ベストが -50 をカウントしたような・・・