Salesforce Apexトリガーのマスター:パフォーマンスとスケーラビリティのためのアーキテクトガイド

背景と適用シナリオ

Salesforce アーキテクトとして、私たちはプラットフォームの堅牢性と拡張性を確保する責任を負っています。その中心的な役割を担うのが Apex Trigger (Apexトリガー) です。Apexトリガーは、レコードがデータベースに挿入、更新、または削除される前後に、カスタムアクションを自動的に実行するための Apex コードです。これは非常に強力なツールですが、その力を誤用すると、パフォーマンスの低下、ユーザーエクスペリエンスの悪化、さらにはシステム全体の不安定化を招く可能性があります。

アーキテクトの視点から見ると、トリガーは単なるコードではなく、ビジネスプロセスを支える重要な自動化コンポーネントです。その設計は、将来の拡張性、保守性、そして Salesforce のマルチテナント環境における厳格な Governor Limits (ガバナ制限) を常に念頭に置いて行う必要があります。

トリガーが不可欠となる一般的なシナリオは以下の通りです:

複雑なデータ検証

標準の入力規則では実現できない、複数のオブジェクトにまたがる複雑なビジネスロジックを実装する場合。例えば、商談のフェーズが「成立」に変更された際、関連する取引先の信用情報が最新であることを確認する、といったシナリオです。

関連レコードの自動更新

あるオブジェクトの変更が、他の関連オブジェクトに影響を与える場合。典型的な例として、取引先の請求先住所が更新されたときに、その取引先に紐づくすべての取引先責任者の郵送先住所を同期させるケースが挙げられます。

関連レコードの自動作成

特定の条件を満たしたときに、新しいレコードを自動的に作成する場合。例えば、重要度が「高」のケースが作成されたときに、担当者にフォローアップを促すためのToDoレコードを自動で作成する、といった自動化です。

複雑な削除ロジックの実行

特定の条件下でレコードの削除を防止したり、削除時にカスタムのクリーンアップ処理を実行したりする場合。例えば、子レコードが存在する親レコードの削除を、特定のプロファイルを持つユーザー以外には許可しない、といった制御です。

これらのシナリオにおいて、トリガーはビジネス要件を満たすための強力なソリューションとなりますが、その設計と実装にはアーキテクチャレベルでの慎重な検討が求められます。


原理説明

Apexトリガーの動作を理解するためには、Salesforce がレコードを保存する際の Order of Execution (実行順序) を把握することが不可欠です。トリガーはこの実行順序の中の特定のポイントで起動します。アーキテクトは、トリガーが他の自動化ツール(入力規則、ワークフロールール、プロセスビルダー、Flowなど)とどのように相互作用するかを正確に理解していなければなりません。

トリガーには、DML操作の「前」に実行される `before` トリガーと、「後」に実行される `after` トリガーの2種類があります。

  • `before` トリガー: レコードがデータベースに保存される前に実行されます。主に、挿入・更新されるレコード自体の値を検証したり、変更したりするために使用されます。このコンテキストでは、レコードの項目を直接変更でき、追加のDML操作なしで変更が保存されるため、効率的です。
  • `after` トリガー: レコードがデータベースに保存された後に実行されます。システムによって割り当てられた項目(Idや最終更新日など)にアクセスしたり、関連オブジェクトのレコードを更新したりする場合に使用されます。関連レコードへの変更には、明示的なDML操作(`update` や `insert` など)が必要です。

トリガー内では、Context Variables (コンテキスト変数) を通じて、実行中のトランザクションに関する情報にアクセスできます。これらはアーキテクチャ設計において極めて重要です。

  • `Trigger.new`: 新しいバージョンのレコードのリスト。`before insert`, `before update`, `after insert`, `after update` トリガーで利用可能です。
  • `Trigger.old`: 古いバージョンのレコードのリスト。`before update`, `before delete`, `after update`, `after delete` トリガーで利用可能です。
  • `Trigger.newMap`: レコードIDをキーとする、新しいバージョンのレコードのMap。`before update`, `after update`, `after delete` トリガーで利用可能です。
  • `Trigger.oldMap`: レコードIDをキーとする、古いバージョンのレコードのMap。`before update`, `before delete`, `after update`, `after delete` トリガーで利用可能です。
  • `Trigger.isInsert`, `Trigger.isUpdate`, `Trigger.isDelete` など: トリガーを起動したDML操作の種類を判別するためのBoolean値。
  • `Trigger.isBefore`, `Trigger.isAfter`: トリガーが `before` イベントか `after` イベントかを判別するためのBoolean値。

アーキテクチャのベストプラクティスとして、「One Trigger Per Object」 (1オブジェクトにつき1トリガー) の原則を強く推奨します。1つのオブジェクトに複数のトリガーが存在すると、実行順序を制御できなくなり、デバッグや保守が非常に困難になります。すべてのロジックを1つのトリガーに集約し、そこから専門のロジックを担う Handler Class (ハンドラクラス) を呼び出す設計パターンを採用することで、コードのモジュール性、再利用性、管理性が劇的に向上します。


示例代码

ここでは、「One Trigger Per Object」パターンとハンドラクラスを用いた、典型的なシナリオのサンプルコードを示します。この例では、取引先 (Account) の「請求先住所(都道府県)」が変更された場合に、関連するすべての取引先責任者 (Contact) の「郵送先住所(都道府県)」を同期させます。このコードは Salesforce の公式ドキュメントで推奨されている設計原則に基づいています。

1. トリガー本体 (AccountTrigger.trigger)

トリガーファイル自体は非常にシンプルに保ちます。その唯一の役割は、実行コンテキストを判別し、適切なハンドラメソッドに処理を委譲することです。

trigger AccountTrigger on Account (after update) {
    // Check if the trigger is in the after update context
    if (Trigger.isAfter && Trigger.isUpdate) {
        // Delegate the logic to the handler class
        AccountTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
    }
}

2. ハンドラクラス (AccountTriggerHandler.cls)

実際のビジネスロジックはすべてこのハンドラクラスに実装します。これにより、ロジックがトリガーの構文から分離され、テストや再利用が容易になります。

public class AccountTriggerHandler {

    /**
     * @description Handles all logic for the 'after update' event on Accounts.
     * @param newAccounts List of Accounts from Trigger.new
     * @param oldAccountMap Map of old Account versions from Trigger.oldMap
     */
    public static void handleAfterUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        
        // 更新対象の取引先責任者を格納するリスト
        List<Contact> contactsToUpdate = new List<Contact>();
        
        // 関連する取引先責任者を問い合わせるための取引先IDのセット
        // Setを使用することで、重複するIDを自動的に排除します
        Set<Id> accountIdsWithAddressChange = new Set<Id>();

        // 更新された取引先をループ処理
        for (Account newAcct : newAccounts) {
            // oldMapから古いバージョンのレコードを取得
            Account oldAcct = oldAccountMap.get(newAcct.Id);

            // 請求先住所(都道府県)が変更されたかどうかを確認
            if (newAcct.BillingState != oldAcct.BillingState) {
                // 変更があった場合、取引先IDをSetに追加
                accountIdsWithAddressChange.add(newAcct.Id);
            }
        }

        // 住所が変更された取引先が存在する場合のみ、後続の処理を実行
        if (!accountIdsWithAddressChange.isEmpty()) {
            
            // 1回のSOQLクエリで、関連するすべての取引先責任者を取得 (Bulkification)
            // ループ内でクエリを発行しないことが極めて重要です
            List<Contact> relatedContacts = [
                SELECT Id, MailingState, AccountId 
                FROM Contact 
                WHERE AccountId IN :accountIdsWithAddressChange
            ];

            // 取得した取引先責任者をループ処理
            for (Contact con : relatedContacts) {
                // トリガーコンテキストから最新の取引先情報を取得
                // このループ内で再度Accountをクエリする必要はありません
                for (Account acc : newAccounts) {
                    if (con.AccountId == acc.Id) {
                        // 郵送先住所(都道府県)を取引先の請求先住所(都道府県)で更新
                        con.MailingState = acc.BillingState;
                        contactsToUpdate.add(con);
                        break; // 一致する取引先が見つかったら内側のループを抜ける
                    }
                }
            }

            // 更新対象の取引先責任者が存在する場合
            if (!contactsToUpdate.isEmpty()) {
                // 1回のDML操作で、すべての取引先責任者を更新 (Bulkification)
                // ループ内でDMLを実行しないことが、ガバナ制限を回避する鍵です
                try {
                    update contactsToUpdate;
                } catch (DmlException e) {
                    // エラーハンドリング: 実際のプロジェクトでは、より洗練されたエラーロギング機構を実装します
                    System.debug('Could not update contacts: ' + e.getMessage());
                }
            }
        }
    }
}

注意事項

アーキテクトとして、トリガーを設計する際には以下の点に最大限の注意を払う必要があります。

Bulkification (一括処理)

これは最も重要な原則です。トリガーは常に200件のレコードを一括で処理できるように設計しなければなりません。データローダーや一括更新APIからのリクエストでは、トリガーは一度に最大200件のレコードのチャンクで実行されます。ループ内でのSOQLクエリやDML操作は絶対に避けてください。 これらはガバナ制限(1トランザクションあたりSOQLクエリ100回、DML操作150回)に抵触する最大の原因です。サンプルコードで示したように、まずIDを収集し、ループの外で一度にクエリやDMLを実行するパターンを徹底してください。

Recursion (再帰処理) の制御

トリガーがレコードを更新し、その更新が同じトリガーを再度起動させてしまうことで、無限ループが発生することがあります。これを再帰呼び出しと呼びます。例えば、取引先トリガーが取引先自体を更新する場合などです。これを防ぐためには、静的変数を用いた制御が一般的です。

public class RecursiveTriggerHandler {
    private static boolean hasAlreadyRun = false;

    public static void executeLogic() {
        if (!hasAlreadyRun) {
            hasAlreadyRun = true;
            // ここに実際のロジックを記述
            // 例: update myAccountList;
        }
    }
}
この静的変数はトランザクションの間だけ値を保持するため、意図しない再帰を防ぐことができます。

非同期処理の活用

トリガーは同期的に実行され、ユーザーがレコードを保存する際の応答時間に直接影響します。そのため、トリガーの処理は可能な限り軽量で高速であるべきです。外部システムへのコールアウトや、複雑で時間のかかる計算処理は、トリガー内から直接実行するべきではありません。このような重い処理は、`@future` メソッドQueueable Apex、またはPlatform Events (プラットフォームイベント) を使用して非同期に実行するように設計します。これにより、ユーザーは即座にUIの制御を取り戻し、バックグラウンドで処理が実行されるため、ユーザーエクスペリエンスが向上します。

エラー処理

`before` トリガーでのビジネスロジック違反は、`addError()` メソッドを使用してユーザーに明確なフィードバックを返し、DML操作を中止させるのが適切です。`after` トリガーでのエラー(特にDML操作の失敗)は `try-catch` ブロックで捕捉し、適切に処理する必要があります。エラーを無視するとデータの不整合を招く可能性があります。また、失敗したトランザクションの情報をカスタムオブジェクトに記録するなど、堅牢なエラーロギング戦略を検討することも重要です。


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

Apexトリガーは、Salesforceプラットフォームの能力を最大限に引き出すための強力なツールですが、その影響範囲は広大です。アーキテクトとしては、目先の要件を満たすだけでなく、システム全体のパフォーマンス、拡張性、保守性を見据えた設計を心がける必要があります。

以下に、トリガー設計における最も重要なベストプラクティスを再度まとめます。

  1. One Trigger Per Object (1オブジェクト1トリガー):

    実行順序を完全に制御し、保守性を高めるために、1つのオブジェクトに対するトリガーは1つに限定します。

  2. Logic-less Triggers (ロジックレスなトリガー):

    トリガー本体にはロジックを記述せず、ハンドラクラスへの処理の委譲のみを行います。これにより、ビジネスロジックの再利用と単体テストが容易になります。

  3. Bulkify Your Code (コードの一括処理):

    常に複数のレコードが一度に処理されることを想定してコードを記述します。これがガバナ制限を遵守するための基本です。

  4. Avoid SOQL/DML in Loops (ループ内でのSOQL/DMLの回避):

    パフォーマンス低下とガバナ制限超過の最大の原因です。コレクション(Set, List, Map)を駆使して、ループの外で一括処理を行います。

  5. Use Maps for Efficient Data Access (効率的なデータアクセスのためのMapの利用):

    クエリ結果や `Trigger.newMap`、`Trigger.oldMap` をMapに格納することで、ネストされたループを避け、関連データへの高速なアクセスを実現します。

  6. Control Recursion (再帰の制御):

    静的変数などを用いて、意図しないトリガーの再帰実行を防止する仕組みを必ず実装します。

  7. Consider Asynchronous Operations (非同期処理の検討):

    時間のかかる処理や外部システム連携は、トリガーから切り離し、非同期プロセスとして実行することで、ユーザーエクスペリエンスとシステムの安定性を保ちます。

これらの原則に従うことで、スケーラブルで、パフォーマンスが高く、そして保守しやすい、堅牢なSalesforceアプリケーションアーキテクチャを構築することが可能になります。

コメント