ApexによるSalesforce承認プロセスのプログラム制御:開発者向けガイド

背景と適用シナリオ

SalesforceのApproval Process (承認プロセス)は、レコードの承認を自動化するための強力な宣言的ツールです。割引申請、経費報告、休暇申請など、組織内の正式な承認フローを標準機能で構築できます。通常、ユーザーはレコードの詳細ページにある「Submit for Approval (承認申請)」ボタンをクリックしてプロセスを開始します。

しかし、ビジネス要件が複雑化するにつれて、標準のユーザーインターフェースだけでは対応できないシナリオが登場します。例えば、以下のようなケースです。

  • カスタムUIからの申請: Lightning Web Component (LWC) や Aura Component で構築されたカスタム画面から、特定のバリデーションロジックを実行した後に承認申請を行いたい。
  • 一括処理: 深夜に実行されるバッチ処理で、特定の条件を満たすすべてのレコードを自動的に承認申請したい。
  • 外部システム連携: 外部システムからのAPIコールをトリガーとして、Salesforce内のレコードの承認プロセスを開始したい。
  • 複雑なビジネスロジック: Apex Trigger内で、レコードの作成または更新時に特定の複雑な条件分岐を経て、承認申請を動的に制御したい。

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


原理説明

Apexで承認プロセスを操作する中心的な役割を担うのが、System名前空間に属するApprovalクラスです。このクラスが提供する静的メソッドを利用することで、レコードの承認申請、承認、却下、取り戻しといったアクションをコードで実行できます。

主な構成要素は以下の2つです。

1. Approval.ProcessRequestオブジェクト

これは、承認プロセスに対してどのようなアクションを実行したいかを定義するためのオブジェクトです。リクエストごとにこのオブジェクトのインスタンスを作成し、必要な情報を設定します。主要なメソッドは以下の通りです。

  • setObjectId(Id recordId): アクションの対象となるレコードのIDを設定します。これは必須の項目です。
  • setComments(String comments): 申請時や承認・却下時のコメントを設定します。
  • setNextApproverIds(List approverIds): 承認ステップで承認者が手動で選択される設定になっている場合、次の承認者のIDをリストで指定します。
  • setProcessDefinitionNameOrId(String nameOrId): どの承認プロセスを使用するかを、そのプロセスの一意の名前(API参照名)またはIDで指定します。複数の承認プロセスが同じオブジェクトに存在する場合に、特定のプロセスを起動するために使用します。
  • setSubmitterId(Id submitterId): 承認の申請者を指定します。指定しない場合、コードを実行しているカレントユーザーが申請者となります。
  • setAction(String action): 実行するアクションを指定します。'Submit' (申請), 'Approve' (承認), 'Reject' (却下), 'Recall' (取り戻し) のいずれかの文字列を指定します。このメソッドは直接は存在せず、Approval.process()メソッドの引数として渡すリクエストリストに対応するアクションを指定します。しかし、実質的にはリクエストがどのアクションであるかを定義する概念です。

2. Approval.ProcessResultオブジェクト

Approval.process()メソッドを実行した結果を格納するオブジェクトです。処理が成功したか、失敗した場合はどのようなエラーが発生したかを確認できます。主要なメソッドは以下の通りです。

  • isSuccess(): 処理が成功したかどうかをBoolean値で返します。
  • getErrors(): 処理が失敗した場合、エラー情報のリスト(Database.Errorオブジェクト)を返します。
  • getInstanceId(): 作成または更新された承認プロセスインスタンスのIDを返します。
  • getInstanceStatus(): 処理後の承認プロセスインスタンスのステータス(例:'Pending', 'Approved')を返します。

開発者は、まずApproval.ProcessRequestオブジェクトのリストを作成し、対象レコードごとに必要な設定を行います。次に、そのリストをApproval.process()メソッドに渡すことで、一括して処理を実行します。この一括処理の考え方は、Salesforceのガバナ制限を回避する上で非常に重要です。


サンプルコード

以下に、特定の取引先 (Account) レコードを承認申請するための基本的なApexコードの例を示します。このコードは、Salesforceの公式ドキュメントに基づいています。

このシナリオでは、すでに'Account_Approval_Process'というAPI参照名の承認プロセスが有効化されていることを前提とします。

// 新しい取引先レコードを作成してデータベースに挿入します。
// 実際のシナリオでは、このレコードは既存のものであることが多いです。
Account acct = new Account(Name='Test Account for Approval');
insert acct;

// 承認申請のためのリクエストオブジェクトを作成します。
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();

// 承認申請の対象となるレコードのIDを設定します。
req.setObjectId(acct.Id);

// 申請時のコメントを設定します。
req.setComments('この取引先を承認申請します。確認をお願いします。');

// 承認プロセスの一意の名前(API参照名)を指定します。
// これにより、同じオブジェクトに複数の承認プロセスがあっても、意図したプロセスを起動できます。
// 省略した場合、Salesforceはエントリ条件に合致する最初のアクティブな承認プロセスを選択しようとします。
req.setProcessDefinitionNameOrId('Account_Approval_Process');

// 申請者を指定します。ここでは現在のユーザーIDを使用しています。
// このユーザーは、承認プロセスの「最初の申請者」として許可されている必要があります。
req.setSubmitterId(UserInfo.getUserId());

// 承認プロセスを実行します。
try {
    Approval.ProcessResult result = Approval.process(req);

    // 処理結果を確認します。
    if (result.isSuccess()) {
        // 成功した場合の処理
        System.debug('承認申請が正常に送信されました。インスタンスID: ' + result.getInstanceId());

        // レコードのステータスが更新されていることを確認するために再クエリします。
        // ただし、承認プロセスによるレコードのロックに注意が必要です。
        // ここではProcessInstanceオブジェクトをクエリしてステータスを確認します。
        ProcessInstance pi = [SELECT Id, Status FROM ProcessInstance WHERE TargetObjectId = :acct.Id ORDER BY CreatedDate DESC LIMIT 1];
        System.debug('現在の承認状況: ' + pi.Status); // 'Pending'などが出力される

    } else {
        // 失敗した場合の処理
        System.debug('承認申請に失敗しました。');
        for (Database.Error err : result.getErrors()) {
            System.debug('エラーメッセージ: ' + err.getMessage());
            // 考えられるエラー:
            // - レコードが承認プロセスのエントリ条件を満たしていない。
            // - 申請者に承認申請の権限がない。
            // - レコードがすでに別の承認プロセスでロックされている。
            System.debug('エラーの項目: ' + err.getFields());
        }
    }
} catch (System.DmlException e) {
    // 予期せぬDMLエラー(例:UNABLE_TO_LOCK_ROW)をキャッチします。
    System.debug('DML例外が発生しました: ' + e.getMessage());
}

注意事項

Apexで承認プロセスを扱う際には、いくつかの重要な点に注意する必要があります。

権限 (Permissions)

Apexコードを実行するユーザーには、適切な権限が必要です。対象オブジェクトへの参照・更新権限はもちろんのこと、システム権限の「承認申請」が有効になっている必要があります。また、承認プロセスの設定で定義された「最初の申請者」に、そのユーザーが含まれているか確認してください。setSubmitterId()で別のユーザーを指定した場合も、その指定されたユーザーが有効な申請者である必要があります。

ガバナ制限 (Governor Limits)

Approval.process()メソッドの呼び出しは、DMLステートメントとしてカウントされます。1回のトランザクションで実行できるDMLステートメントの回数には上限(同期Apexでは150回)があります。したがって、複数のレコードを処理する場合は、必ずforループ内でメソッドを呼び出すのではなく、ProcessRequestのリストを作成し、一度のApproval.process()呼び出しで一括処理してください。このメソッドは最大100件のリクエストを一度に処理できます。

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

レコードが承認プロセスに入ると、そのレコードはロックされ、承認者とシステム管理者以外のユーザーは通常編集できなくなります。Apexコード内で承認申請を行った直後に同じレコードを更新しようとすると、UNABLE_TO_LOCK_ROWエラーが発生する可能性があります。このロック動作を考慮して、トランザクションの順序やビジネスロジックを設計する必要があります。

エラー処理 (Error Handling)

Approval.process()メソッドは、必ずしもすべてのリクエストが成功するとは限りません。try-catchブロックで例外を捕捉するだけでなく、返り値であるProcessResultオブジェクトのisSuccess()メソッドを必ず確認し、失敗した場合はgetErrors()で原因を特定・記録するロジックを実装することが不可欠です。一般的なエラーには、「エントリ条件を満たしていない」「申請者が無効」「レコードがロックされている」などがあります。


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

ApexのApprovalクラスは、Salesforceの標準的な承認プロセスをプログラムで柔軟に拡張するための強力なツールです。カスタムUIやバッチ処理、外部連携など、宣言的機能だけでは実現が難しい要件に対応する道を開きます。

開発者として承認プロセスを扱う際のベストプラクティスは以下の通りです。

  1. 常に一括処理を意識する (Bulkification): 複数のレコードを扱う際は、必ずProcessRequestのリストを作成し、単一のApproval.process()コールで処理します。これにより、ガバナ制限の抵触を防ぎ、パフォーマンスを向上させます。
  2. 堅牢なエラーハンドリングを実装する: ProcessResultを精査し、部分的な成功や失敗に対応できるロジックを構築します。エラーログを適切に記録し、問題の追跡を容易にします。
  3. 宣言的アプローチを優先する: 要件がフローなどの宣言的ツールで実現可能であれば、そちらを優先します。コードは保守コストが高くなるため、真にプログラムによる制御が必要な場合にのみApexを使用します。
  4. テストを徹底する: Apexトリガーと同様に、承認プロセスを呼び出すコードも網羅的なテストが必要です。正常系(承認申請が成功するケース)だけでなく、異常系(エントリ条件を満たさない、権限がないユーザーが実行するなど)のシナリオもテストクラスで検証してください。Test.startTest()Test.stopTest()を使用して、非同期処理とガバナ制限をリセットし、正確なテストを実施します。

これらの原理とベストプラクティスを理解し、適切に活用することで、Salesforceプラットフォーム上により洗練され、自動化されたビジネスプロセスを構築できるでしょう。

コメント