ApexによるSalesforce承認プロセスの自動化:開発者向け詳細ガイド

背景と適用シナリオ

Salesforceにおける Approval Process (承認プロセス) は、組織内のレコード承認を自動化するための強力な宣言的ツールです。割引申請、経費精算、休暇申請など、特定の条件を満たしたレコードが承認を得るまでの一連のステップを定義します。通常、ユーザーはレコードの詳細ページにある「Submit for Approval (承認申請)」ボタンをクリックしてプロセスを開始します。

しかし、ビジネスロジックが複雑化するにつれて、標準のUI操作だけでは要件を満たせないケースが増えてきます。例えば、以下のようなシナリオが考えられます。

  • カスタムUIからの申請: Lightning Web Components (LWC) や Aura Components で構築されたカスタム画面から、ユーザー操作に応じて承認プロセスを起動したい。
  • 一括処理: 特定の条件を満たした複数のレコードを、夜間バッチ処理などで一括して承認申請したい。
  • 外部システム連携: 外部システムからのAPIコールをトリガーとして、Salesforce内のレコードを承認プロセスに投入したい。
  • 複雑な条件分岐: レコードのデータだけでなく、関連する複数のオブジェクトの状態を考慮した上で、動的に承認申請の要否を判断したい。

このような高度な要件に対応するため、Salesforceは開発者向けに Apex を使用して承認プロセスをプログラム的に制御する手段を提供しています。この記事では、私、Salesforce開発者の視点から、Apexを用いて承認プロセスを操作する方法について、その原理から具体的な実装、注意点までを詳細に解説します。


原理説明

Apexから承認プロセスを制御する中核となるのが、Approval という名前空間に用意されたクラス群です。これにより、レコードの承認申請、承認、却下といった操作をコードで実行できます。

プログラムによる承認申請の基本的な流れは以下の通りです。

  1. 申請リクエストの作成: Approval.ProcessSubmitRequest オブジェクトをインスタンス化します。これは、承認申請に関するすべての情報を格納するコンテナの役割を果たします。
  2. リクエストへの情報設定: 作成したリクエストオブジェクトに、どのレコードを、誰が、どのようなコメントと共に申請するのか、といった情報を設定します。
    • setObjectId(Id): 承認申請するレコードのIDを設定します。
    • setComments(String): 申請時のコメントを設定します。
    • setSubmitterId(Id): 申請者のIDを設定します。省略した場合、コードを実行しているカレントユーザーが申請者となります。
    • setNextApproverIds(List<Id>): 承認プロセスの設定で承認者を「申請者が選択」としている場合に、次の承認者のIDリストを指定します。
  3. プロセスの実行: Approval.process() メソッドを呼び出し、作成したリクエストオブジェクトを引数として渡します。このメソッドが実際に承認プロセスを開始または操作します。
  4. 結果の確認: Approval.process() メソッドは Approval.ProcessResult オブジェクトを返します。このオブジェクトの isSuccess() メソッドで処理の成否を確認したり、getErrors() メソッドでエラー情報を取得したりできます。

この一連の流れをApexコードで実装することで、前述したようなカスタムUIやバッチ処理からの動的な承認申請が可能になります。重要なのは、この方法は既存の承認プロセスの定義(どのプロセスが起動されるかは申請条件に依存)に従って動作するという点です。Apexはあくまでプロセスの「トリガー」であり、プロセスのステップやアクション自体は、引き続き「設定」画面で管理します。


サンプルコード

ここでは、特定の商談(Opportunity)レコードをApex経由で承認申請する具体的なコード例を示します。シナリオとして、「割引率が20%を超える商談を自動的に承認申請するトリガー」を想定してみましょう。

このコードは、商談が作成または更新された際に、指定した条件を満たしていれば自動で承認プロセスに投入します。

商談トリガーの例

trigger OpportunityApprovalTrigger on Opportunity (after insert, after update) {
    for (Opportunity opp : Trigger.new) {
        // 以前のレコードと比較し、割引率が変更されたか、新規作成されたレコードかを確認
        Opportunity oldOpp = Trigger.isUpdate ? Trigger.oldMap.get(opp.Id) : null;
        
        // 割引率が20%を超えており、まだ承認プロセスに乗っていない場合に処理を実行
        if (opp.Discount_Percentage__c > 20 && !isRecordInApproval(opp.Id)) {
            
            // 承認申請リクエストを作成
            Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
            
            // 申請時のコメントを設定
            req.setComments('割引率が20%を超えたため、自動で承認申請します。');
            
            // 承認申請の対象となるレコードIDを設定
            req.setObjectId(opp.Id);
            
            // 申請者をレコードの所有者に設定
            // これにより、誰がトリガーを実行したかに関わらず、所有者が申請したことになる
            req.setSubmitterId(opp.OwnerId);

            try {
                // 承認プロセスを開始
                Approval.ProcessResult result = Approval.process(req);

                // 処理結果を確認
                if (result.isSuccess()) {
                    System.debug('商談 ' + opp.Name + ' が正常に承認申請されました。');
                    
                    // 成功した場合、カスタム項目などでステータスを更新することも可能
                    // opp.Approval_Status__c = '申請中';
                    
                } else {
                    // 承認申請が失敗した場合の処理
                    System.debug('承認申請に失敗しました。エラー: ');
                    for(Approval.ProcessResult.Error error : result.getErrors()) {
                        System.debug('項目: ' + error.getField());
                        System.debug('メッセージ: ' + error.getMessage());
                    }
                }
            } catch (System.DmlException e) {
                // DML例外(ロックエラーなど)の処理
                System.debug('承認申請中に予期せぬエラーが発生しました: ' + e.getMessage());
            }
        }
    }

    // レコードが既に承認プロセス内にあるかを確認するヘルパーメソッド
    private static Boolean isRecordInApproval(Id recordId) {
        List<ProcessInstance> piList = [SELECT Id FROM ProcessInstance WHERE TargetObjectId = :recordId AND Status = 'Pending'];
        return !piList.isEmpty();
    }
}

コードの解説:

  • 1-2行目: 商談オブジェクトの作成後・更新後イベントで発火するトリガーを定義します。
  • 7-8行目: 割引率が20%を超え、かつヘルパーメソッド isRecordInApproval でまだ承認プロセスに入っていないことを確認します。重複申請を防ぐためにこのチェックは非常に重要です。
  • 11-20行目: Approval.ProcessSubmitRequest オブジェクトを準備し、コメント、対象レコードID、申請者IDを設定しています。ここでは申請者をレコード所有者に指定しています。
  • 22-38行目: try-catch ブロック内で Approval.process(req) を実行します。これにより、承認プロセスが開始されます。戻り値である ProcessResult を用いて、成功したか、失敗した場合はどのようなエラーが発生したかをログに出力しています。
  • 42-45行目: isRecordInApproval メソッドは、指定されたレコードIDが有効な承認プロセスインスタンス(ProcessInstance)に存在するかをクエリして判定します。ステータスが 'Pending' のものを探すのが一般的です。


注意事項

Apexで承認プロセスを扱う際には、いくつかの重要な点に注意する必要があります。これらを怠ると、予期せぬエラーやガバナ制限超過の原因となります。

権限 (Permissions)

Approval.process() メソッドを実行するユーザー、または setSubmitterId() で指定されたユーザーは、対象オブジェクトに対する「承認申請 (Submit for Approval)」権限を持っている必要があります。この権限がない場合、DmlException が発生し、「Submit for approval requires approval access」といったエラーメッセージが表示されます。プロファイルや権限セットで適切な設定がされていることを確認してください。

API 制限とガバナ制限 (API and Governor Limits)

Apexによる承認プロセスの操作は、Salesforceのガバナ制限の対象となります。

  • DML ステートメント: Approval.process() の呼び出しは、1回のコールにつき1つの DML (Data Manipulation Language) ステートメントとしてカウントされます。トリガー内でループ処理を行う場合、1トランザクションあたりのDML制限(150回)に達しないように注意が必要です。
  • 承認アクション: 承認プロセス内で定義されているアクション(メール送信、項目自動更新、ToDo作成など)も、それぞれガバナ制限を消費します。特に項目自動更新は追加のDMLやCPU時間を消費するため、複雑なプロセスでは全体のトランザクションに与える影響を考慮する必要があります。
  • 一括処理の考慮: Approval.process() メソッドは一括処理に対応していません。つまり、リストを渡して一度に複数のレコードを申請することはできません。複数のレコードを申請する場合は、ループ内で1件ずつ呼び出す必要があります。このため、バッチApexなどで大量のレコードを処理する際は、ガバナ制限に抵触しないよう慎重な設計が求められます。

エラー処理 (Error Handling)

承認申請は、様々な理由で失敗する可能性があります。例えば、レコードが承認プロセスのエントリ条件を満たしていない、レコードがロックされている、申請者に権限がない、などです。そのため、Approval.process() の呼び出しは必ず try-catch ブロックで囲み、例外を適切に捕捉してください。また、例外が発生しなくても処理が失敗する場合があるため、戻り値である ProcessResultisSuccess() を必ずチェックし、エラーがあればその内容をログに記録したり、ユーザーにフィードバックしたりする実装が不可欠です。

レコードのロック (Record Locking)

レコードが承認プロセスに申請されると、そのレコードはロックされ、管理者と現在の承認者以外は編集できなくなります。Apexコードからこのロックされたレコードを更新しようとすると、UNABLE_TO_LOCK_ROW というエラーが発生します。この挙動を理解し、承認プロセス中のレコードを不用意に更新しないようにコードを設計する必要があります。


まとめとベストプラクティス

Salesforceの承認プロセスは、Apexと組み合わせることで、標準機能の枠を超えた柔軟で強力な自動化ソリューションを構築することができます。カスタムUIからの動的な申請や、複雑なビジネスロジックに基づく自動申請など、その可能性は多岐にわたります。

最後に、開発者として承認プロセスを扱う上でのベストプラクティスをいくつか紹介します。

1. 宣言的アプローチを第一に (Declarative First)

要件がプロセスビルダーやフローなどの宣言的ツールで実現可能であれば、そちらを優先すべきです。コードはメンテナンスコストが高くなるため、Apexは宣言的ツールではどうしても実現不可能な場合に限定して使用するのが賢明です。

2. IDのハードコーディングを避ける (Avoid Hardcoding IDs)

コード内に承認者のユーザーIDや承認プロセスのIDを直接書き込む(ハードコーディングする)のは避けるべきです。これらのIDはSandboxと本番環境で異なる可能性があり、メンテナンス性を著しく低下させます。承認者IDはカスタム設定やカスタムメタデータ型に保存したり、User オブジェクトから動的に取得したりする方法が推奨されます。承認プロセスの定義IDが必要な場合は、ProcessDefinition オブジェクトをSOQLで検索して取得しましょう。

3. 再利用可能なサービスの構築 (Build Reusable Services)

承認申請ロジックをトリガー内に直接記述するのではなく、独立したApexクラス(サービスクラス)のメソッドとして実装し、トリガーからはそのメソッドを呼び出すように設計します。これにより、コードの再利用性が高まり、単体テストも容易になります。

4. 包括的なテスト (Comprehensive Testing)

承認プロセスに関連するApexコードのテストクラスでは、単にコードが実行されるだけでなく、実際の承認フローをシミュレートすることが重要です。

  • テスト用の承認プロセスを有効化しておく必要があります。
  • レコードを申請した後、ProcessInstanceProcessInstanceWorkitem オブジェクトをSOQLで照会し、レコードが期待通りに正しいステップに進んでいるか、正しいユーザーに承認依頼が割り当てられているかなどをアサーションで検証します。
  • 成功ケースだけでなく、申請が失敗するケース(エントリ条件を満たさない、権限がない等)も網羅的にテストします。

これらの原理とベストプラクティスを理解し実践することで、Salesforce開発者として、より堅牢でスケーラブルな承認プロセスの自動化を実現できるでしょう。

コメント