Salesforce Apexトリガ:ベストプラクティスと実装の詳細解説

背景と応用シナリオ

Salesforceプラットフォームにおいて、標準機能やフローなどの宣言的ツールだけでは実現できない複雑なビジネスロジックを実装する必要がある場合、Apex Trigger (Apexトリガ) が強力なソリューションとなります。Apexトリガは、Salesforceのレコード(Account, Contact, Custom Objectなど)が作成、更新、または削除される前(before)または後(after)に、カスタムのApexコードを自動的に実行するための仕組みです。

具体的な応用シナリオとしては、以下のようなものが挙げられます。

1. 高度な入力規則: 複数のオブジェクトをまたがるような複雑な条件でのデータ検証。例えば、「取引先に有効な商談が存在する場合、その取引先の種別を『顧客』以外に変更できない」といったルールを実装します。

2. 関連レコードの自動更新: あるレコードの変更が、他の関連レコードに影響を及ぼす場合の自動処理。例えば、「取引先の住所が変更されたら、その取引先に紐づくすべての取引先責任者の郵送先住所も自動で更新する」といったシナリオです。

3. 外部システム連携: レコードが特定の条件を満たした際に、外部システムのAPIを呼び出してデータを同期するなど、宣言的ツールでは難しい連携処理を実装します。

Apexトリガは非常に柔軟性が高い一方で、ガバナ制限や実行順序など、考慮すべき点が多く存在します。そのため、その仕組みを正しく理解し、ベストプラクティスに沿って設計・実装することが、安定的でスケーラブルなシステムを構築する上で不可欠です。


原理説明

Apexトリガは特定のオブジェクトと、一つ以上のイベントに関連付けて定義されます。トリガの基本的な構文は以下の通りです。
trigger TriggerName on ObjectName (trigger_events) { ... }

トリガイベント
トリガが起動するタイミングは、以下の7つのイベントによって指定されます。

before insert: レコードがデータベースに保存される前に実行されます。
before update: レコードがデータベースで更新される前に実行されます。
before delete: レコードがデータベースから削除される前に実行されます。
after insert: レコードがデータベースに保存された直後に実行されます。この時点ではレコードIDが確定しています。
after update: レコードがデータベースで更新された直後に実行されます。
after delete: レコードがデータベースから削除された直後に実行されます。
after undelete: レコードがごみ箱から復元された直後に実行されます。

トリガコンテキスト変数 (Trigger Context Variables)

トリガの実行中、Apexコードは特別なContext Variables (コンテキスト変数) を通じて、処理対象のレコード情報にアクセスできます。これらの変数を適切に使い分けることが、効率的なトリガを実装する鍵となります。

Trigger.new:
insertおよびupdateイベントで利用可能。処理対象の新しいバージョンのsObjectレコードのリストです。beforeトリガでは、このリスト内のレコードの項目値を変更することができます。

Trigger.old:
updateおよびdeleteイベントで利用可能。処理対象の古いバージョン(変更前)のsObjectレコードのリストです。

Trigger.newMap:
after insert, before/after updateイベントで利用可能。キーがレコードID、値が新しいバージョンのsObjectレコードであるMapです。

Trigger.oldMap:
updateおよびdeleteイベントで利用可能。キーがレコードID、値が古いバージョンのsObjectレコードであるMapです。

Trigger.isExecuting:
現在のコードがトリガによって実行されている場合にtrueを返します。

Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete, etc.:
現在のトリガイベントを判定するためのboolean型の変数です。

Trigger.size:
トリガが処理しているレコードの総数を返します。


示例代码

ここでは、「取引先 (Account) の『年間売上 (AnnualRevenue)』が更新された場合、その取引先に紐づくすべての商談 (Opportunity) の『説明 (Description)』項目に、取引先の売上情報を追記する」というシナリオを実装します。このコードは、bulkification (一括処理) のベストプラクティスに従っています。

トリガ本体 (AccountTrigger.apxt)

trigger AccountTrigger on Account (after update) {
    // after update イベントでのみロジックを実行
    if (Trigger.isAfter && Trigger.isUpdate) {
        // ビジネスロジックをハンドラクラスに委譲する
        AccountTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
    }
}

ハンドラクラス (AccountTriggerHandler.apxc)

public class AccountTriggerHandler {

    // after update イベントのロジックを処理するメソッド
    public static void handleAfterUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        
        // 関連する商談を更新するためのリストを初期化
        List<Opportunity> opportunitiesToUpdate = new List<Opportunity>();
        
        // 年間売上が変更された取引先のIDを格納するSet
        Set<Id> accountIdsWithRevenueChange = new Set<Id>();

        // 更新されたすべての取引先をループ処理
        for (Account newAcc : newAccounts) {
            // 変更前の取引先情報を取得
            Account oldAcc = oldAccountMap.get(newAcc.Id);

            // AnnualRevenue が変更されたかどうかを確認(nullからの変更も考慮)
            if (newAcc.AnnualRevenue != oldAcc.AnnualRevenue) {
                accountIdsWithRevenueChange.add(newAcc.Id);
            }
        }

        // AnnualRevenueが変更された取引先が存在する場合のみ、SOQLを実行
        if (!accountIdsWithRevenueChange.isEmpty()) {
            // 関連するすべての商談を一括で取得する (SOQLクエリはループの外で実行)
            List<Opportunity> relatedOpportunities = [
                SELECT Id, Description, AccountId, Account.Name, Account.AnnualRevenue 
                FROM Opportunity 
                WHERE AccountId IN :accountIdsWithRevenueChange
            ];

            // 取得した商談をループ処理
            for (Opportunity opp : relatedOpportunities) {
                // 説明項目を更新
                opp.Description = '親取引先「' + opp.Account.Name + '」の年間売上が更新されました。' +
                                  '新しい売上: ' + opp.Account.AnnualRevenue;
                
                // 更新対象の商談リストに追加
                opportunitiesToUpdate.add(opp);
            }
        }

        // 更新対象の商談が存在する場合のみ、DML操作を実行
        if (!opportunitiesToUpdate.isEmpty()) {
            // 商談リストを一括で更新する (DML操作はループの外で実行)
            try {
                update opportunitiesToUpdate;
            } catch (DmlException e) {
                // エラーハンドリング: 実際にはログ出力や管理者に通知する処理を記述
                System.debug('商談の更新に失敗しました: ' + e.getMessage());
            }
        }
    }
}

注意事項

ガバナ制限 (Governor Limits)

Salesforceはマルチテナント環境であるため、すべてのユーザが安定してプラットフォームを利用できるよう、1回のトランザクションで実行できる処理量にGovernor Limits (ガバナ制限) が設けられています。トリガを実装する際は、これらの制限を常に意識する必要があります。

・SOQLクエリの発行回数: 同期処理では100回まで。
・DMLステートメントの発行回数: 150回まで。
・DML操作で処理できるレコード総数: 10,000件まで。
・CPU実行時間: 10,000ミリ秒まで。

これらの制限を超えると、トランザクション全体がロールバックされ、エラーが発生します。特に、`for`ループの中でSOQL (Salesforce Object Query Language) クエリやDML (Data Manipulation Language) 操作を実行することは、最も典型的な制限超過の原因となるため、絶対に避けるべきです。

再帰実行 (Recursion)

トリガがレコードを更新し、その更新が原因で同じトリガが再度呼び出される「再帰実行」が発生することがあります。例えば、`Account`の`after update`トリガ内で、処理対象の`Account`レコードを再度`update`すると、無限ループに陥る可能性があります。これを防ぐには、以下のように静的変数を用いた制御が一般的です。

public class TriggerControl {
    public static boolean isTriggerRunning = false;
}

// トリガの先頭でチェック
if (TriggerControl.isTriggerRunning) {
    return;
}
TriggerControl.isTriggerRunning = true;
// ... トリガのロジック ...

エラーハンドリング (Error Handling)

DML操作は失敗する可能性があるため、必ず`try-catch`ブロックで囲み、例外を適切に処理することが重要です。また、特定のレコードの保存を意図的に失敗させ、ユーザにエラーメッセージを表示したい場合は、`sObject.addError()`メソッドを使用します。このメソッドを使うと、レコードの保存が中止され、UI上にメッセージが表示されます。


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

高品質でメンテナンス性の高いApexトリガを実装するために、以下のベストプラクティスを遵守することを強く推奨します。

1. One Trigger Per Object (1オブジェクトにつき1トリガ):
1つのオブジェクトに対して複数のトリガを作成すると、実行順序が保証されず、デバッグや管理が非常に困難になります。すべてのロジックを1つのトリガに集約し、その中でイベントや条件に応じて処理を分岐させるように設計してください。

2. ロジックをハンドラクラスに分離する (Logic-less Triggers):
トリガファイル自体にはロジックを記述せず、トリガイベントに応じてハンドラクラスのメソッドを呼び出すだけのシンプルな構造にします。これにより、ロジックの再利用性が高まり、単体テストも容易になります。上記のサンプルコードがこのパターンの一例です。

3. コードの一括処理 (Bulkify Your Code):
トリガは常に複数のレコード(最大200件)を一度に処理する可能性があることを前提に設計します。絶対にループ内でSOQLやDMLを実行しないでください。`Set`や`Map`を効果的に活用し、処理を効率化します。

4. 網羅的なテストクラスの作成:
Apexトリガを本番環境にデプロイするには、75%以上のコードカバー率を満たすテストクラスが必須です。単体レコードの処理、複数レコードの一括処理、エラーケースなど、様々なシナリオを想定したテストを記述し、品質を担保してください。

ApexトリガはSalesforce開発における強力なツールですが、その力を最大限に引き出すためには、プラットフォームの制約とアーキテクチャの原則を深く理解することが不可欠です。本記事で紹介した原則とベストプラクティスを参考に、堅牢でスケーラブルなアプリケーションを構築してください。

コメント