【Salesforce】Apexトリガのベストプラクティス&サンプルテンプレート

この記事でわかること
  • Apexトリガ開発のベストプラクティス
  • Apexトリガのサンプルテンプレート

今回はApexトリガを開発する際のベストプラクティスについて解説したいと思います。

なしば
なしば

どのプロジェクトでも使えるサンプルテンプレートも紹介します!

Apexトリガ開発時のベストプラクティス

Apexトリガ開発の際は次のようなことを心がけましょう。

  • フローや入力規則では要件を実現できない場合に実装する
  • 1つのオブジェクトにApexトリガは1つだけ実装する
  • Apexトリガに対応するトリガハンドラを実装する
  • 一括変更を考慮した設計にする
  • トリガの実行順序に注意する

それぞれ解説します。

フローや入力規則では要件を実現できない場合に実装する

Salesforce開発においては、なるべくフローや入力規則などのいわゆる標準機能を利用するのが望ましいです。

標準機能では実現が難しい要件が発生した場合のみ、Apexトリガを使用した開発を検討するようにしましょう。

1つのオブジェクトにApexトリガは1つだけ実装する

1つのオブジェクトに複数のトリガが実装されている場合、トリガの実行順序は制御できません。

そのため、1つのオブジェクトには1つのトリガだけを実装し、常に一定の順序で処理が行われるようにするのが望ましいです。

Apexトリガに対応するトリガハンドラを実装する

Apexトリガ内に複雑な処理を実装するのは避けるのが望ましいです。

単純化のため、Apexクラスを別途作成して、before insertやafter updateなどのイベント別に呼び出されるメソッドを実装し、Apexトリガから呼び出すようにするのが良いでしょう。

なしば
なしば

後ほどサンプルを紹介します!

一括変更を考慮した設計にする

レコードの変更は標準の編集画面からだけでなく、プログラムやデータローダなどのツールから複数レコード一括で行われる可能性もあります。

そのため、Apexトリガはレコードを1件1件処理するのではなく、ListやMapを使って複数レコードをまとめて処理できる作りにしておく必要があります。

NGな例
Account acc = Trigger.new[0];
acc.Flg__c = true; 

上記のような書き方をしてしまうと、一括変更されたレコードの最初の1件しか処理されないことになってしまいます。

OKな例
for(Account acc : Trigger.new){
    acc.Flg__c = true; 
}

このようにfor loopを使用して、すべてのレコードに対して処理が行われるようにしてください。

なしば
なしば

たとえ編集画面からしか更新が想定されない場合でも、一括変更を想定した作りにしておくことが望ましいと思います。

トリガの実行順序に注意する

Salesforceでは下記の順序で処理が行われます。

  1. 元のレコードがデータベースから読み込まれるか、upsert ステートメント用にレコードが初期設定されます。
  2. 要求から新しいレコード項目の値が読み込まれ、古い値を上書きします。要求が標準 UI 編集ページから行われた場合は、Salesforce がシステム検証を実行して、レコードについて次の点を確認します。
    • レイアウト固有のルールへの準拠
    • レイアウトレベルおよび項目定義レベルで必要な値
    • 有効な項目形式
    • 最大項目サイズ要求が Apex アプリケーションや SOAP API コールなど他の提供元から送信されている場合、Salesforce では外部キーのみを検証します。トリガを実行する前に、Salesforce がカスタム外部キーがオブジェクト自体を参照しないことを確認します。見積品目や商談品目など、複数行の品目が作成された場合、Salesforce はカスタムの入力規則を実行します。
  3. レコードの保存前に実行されるように設定されたレコードトリガフローを実行します。
  4. すべての before トリガが実行されます。
  5. すべての必須項目に null 以外の値が入力されていることの確認や、カスタムの入力規則の実行など、システム検証のほとんどの手順がもう一度実行されます。Salesforce が標準 UI + 編集ページから要求が行われた場合に再度実行しない唯一のシステム検証は、レイアウト固有のルールの適用です。
  6. 重複ルールが実行されます。重複ルールが重複するレコードを特定してブロックアクションを実行した場合は、レコードが保存されず、after トリガやワークフロールールなどの後続のステップが実行されません。
  7. レコードはデータベースに保存されますが、まだ確定されません。
  8. すべての after トリガが実行されます。
  9. 割り当てルールが実行されます。
  10. 自動応答ルールが実行されます。
  11. ワークフロールールが実行されます。ワークフロー項目自動更新が存在する場合、次の実行が行われます。
    1. レコードが再度更新されます。
    2. システム検証が再度実行されます。カスタム入力規則、フロー、重複ルール、プロセスおよびエスカレーションルールは再実行されません。
    3. レコードの操作 (挿入または更新) に関わらず、before update トリガと after update トリガがもう 1 回 (1 回だけ) 実行されます。
  12. エスカレーションルールが実行されます。
  13. 次の Salesforce フロー自動化を実行しますが、順序は保証されません。
    • プロセス
    • プロセスによって起動されたフロープロセスまたはフローで DML 操作が実行されるときに、影響を受けるレコードに対して保存手順が実行されます。
  14. エンタイトルメントルールが実行されます。
  15. レコードの保存後に実行されるように設定されたレコードトリガフローを実行します。
  16. レコードに積み上げ集計項目が含まれる場合、またはレコードがクロスオブジェクトワークフローの一部である場合、計算が実行され、親レコードの積み上げ集計項目が更新されます。親レコードに対して保存手順が実行されます。
  17. 親レコードが更新され、さらにその親レコードに積み上げ集計項目が含まれるか、その親レコードがクロスオブジェクトワークフローの一部である場合、計算が実行され、親の親レコードの積み上げ集計項目が更新されます。親の親レコードに対して保存手順が実行されます。
  18. 条件に基づく共有の評価が実行されます。
  19. すべての DML 操作がデータベースで確定されます。
  20. 変更がデータベースにコミットされた後、メール送信などのコミット後ロジックを実行し、キューに追加された非同期 Apex ジョブ (キュー可能ジョブや future メソッドなど) を実行します。

例えば、4番目のbeforeトリガは7番のレコードのデータベースへの保存より前に動くため、before insertトリガの処理内でトリガ処理対象レコードのIDを使用したいとします。

しかし、IDはデータベースへ保存されたタイミングで採番されるため、before inserの時点で参照してもnullが返却されてしまいます。

なしば
なしば

このように処理するタイミングによっては想定と異なる結果になってしまう可能性があるので注意しましょう。

Apexトリガのサンプルテンプレート

Apexトリガのサンプルテンプレートを紹介します。

サンプルは取引先のトリガなので、実装するオブジェクトに置き換えて使用してください。

Apex トリガ
trigger AccountTrigger on Account  (after delete,
                                    after insert,
                                    after undelete,
                                    after update,
                                    before delete,
                                    before insert,
                                    before update) {
    
    if(AccountHandlerTrigger.hasExecuting || TriggerSwitch__mdt.getInstance('Account').IsTriggerOff__c){
        return; // 再帰時またはトリガオフフラグがたっている場合はスキップ
    }
    
    AccountHandlerTrigger.hasExecuting = false; // 再帰処理防止用フラグをオン
    AccountHandlerTrigger handler = new AccountHandlerTrigger(Trigger.isExecuting, Trigger.size);               

    if(Trigger.isInsert && Trigger.isBefore){
        handler.onBeforeInsert(Trigger.new);

    }else if(Trigger.isInsert && Trigger.isAfter){
        handler.onAfterInsert(Trigger.new, Trigger.newMap);

    }else if(Trigger.isUpdate && Trigger.isBefore){
        handler.onBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap);

    }else if(Trigger.isUpdate && Trigger.isAfter){
        handler.onAfterUpdate(Trigger.old, Trigger.new, Trigger.newMap);

    }else if(Trigger.isDelete && Trigger.isBefore){
        handler.onBeforeDelete(Trigger.old, Trigger.oldMap);

    }else if(Trigger.isDelete && Trigger.isAfter){
        handler.onAfterDelete(Trigger.old, Trigger.oldMap);

    }else if(Trigger.isUnDelete){
        handler.onBeforeDelete(Trigger.new, Trigger.newMap);
    }
}
Apex Class(トリガハンドラ)
public class AccountHandlerTrigger {
    public static Boolean hasExecuting = false; // 再帰処理防止
    private Boolean m_isExecuting = false; // 
    private Integer BatchSize = 0;

    public AccountHandlerTrigger(Boolean isExecuting, Integer size){
        m_isExecuting = isExecuting;
        BatchSize = size;
    }
    
    /**
     * Before Insert 
    */
    public void onBeforeInsert(List<Account> newAccounts){
        // 処理
    }

    /**
     * After Insert 
    */
    public void onAfterInsert(List<Account> newAccounts, Map<Id, Account> accountMap){
        // 処理
    }

    
    /**
     * Before Update 
    */
    public void onBeforeUpdate(List<Account> oldAccounts, List<Account> updatedAccounts, Map<Id, Account> newAccountMap){
        // 処理
    }
    
    /**
     * After Update 
    */
    public void onAfterUpdate(List<Account> oldAccounts, List<Account> updatedAccounts, Map<Id, Account> newAccountMap){
        // 処理
    }

    /**
     * Before Delete
    */
    public void onBeforeDelete(List<Account> accountsToDelete, Map<Id, Account> accountMap){
        // 処理
    }
    
    /**
     * After Delete
    */
    public void onAfterDelete(List<Account> deletedAccounts, Map<Id, Account> accountMap){
        // 処理
    }
    
    /**
     * Undelete
    */
    public void onUndelete(List<Account> restoredAccounts){
        // 処理
    }
}

実際の処理を行うトリガハンドラクラスをイベントごとにメソッドを分けて作成しておき、Apexトリガはイベントに応じたトリガハンドラのメソッドを呼び出すだけの役割になります。

補足① 再帰処理防止フラグを立てる

Apex トリガ
    if(AccountHandlerTrigger.hasExecuting || TriggerSwitch__mdt.getInstance('Account').IsTriggerOff__c){
        return; // 再帰時またはトリガオフフラグがたっている場合はスキップ
    }
    
    AccountHandlerTrigger.hasExecuting = false; // 再帰処理防止用フラグをオン
    AccountHandlerTrigger handler = new AccountHandlerTrigger(Trigger.isExecuting, Trigger.size);

トリガ処理の最初にトリガハンドラ側に用意しているstaticな変数「AccountHandlerTrigger.hasExecuting」で判定を行います。

初めて一番最初に処理が行われる際は当然値はfalseのため、return処理は行われません。

その後、「AccountHandlerTrigger.hasExecuting」にtrueが設定されています。

一度処理が行われたオブジェクトのレコードが何らかの理由で更新された場合、再度トリガ処理が動きますが、上記のフラグがtrueになっているので、2回目以降の処理は行われなくなります。

このように再帰処理防止フラグを用意してトリガの最初で判定を行うことで、数珠繋ぎトリガによる無限ループなどを防ぐことができます。

補足② トリガのオン/オフフラグを用意しておく

Apexトリガのオンオフを切り変える場合、Sandbox環境でオフにしたトリガを本番環境にデプロイするなどの手間が発生します。

データのメンテナンスなどでトリガを一時的にオフにしたい時にいちいち面倒ですよね。

そんな時のために、設定一つでトリガのオン/オフを切り替えられる仕組みを用意しておきましょう。

カスタムメタデータ型

あらかじめこのようなカスタムメタデータを定義しておきます。

Apex トリガ
    if(AccountHandlerTrigger.hasExecuting || TriggerSwitch__mdt.getInstance('Account').IsTriggerOff__c){
        return; // 再帰時またはトリガオフフラグがたっている場合はスキップ
    }

再帰防止フラグと同様にトリガ処理の最初でカスタムメタデータから該当オブジェクトのレコードを取得してフラグの値を判定します。

こうすることで、カスタムメタデータのレコードのフラグを切り替えるだけで、トリガ処理のオンオフを切り替えることができるようになり、保守性が向上します。

なしば
なしば

カスタムメタデータじゃなくても、カスタム設定やカスタム表示ラベルでも何でも良いです。とにかくデプロイなしでオンオフを切り替えられるようにできるのが重要です。

まとめ

今回はApexトリガのベストプラクティスとサンプルテンプレートの紹介をしました。

同一組織内で書き方がバラバラだと保守性が悪くなるので、コーディング規約などであらかじめルールを決めておくのが望ましいと思います。

今回紹介したテンプレートを上手に活用して、効率の良いトリガを作成できるように頑張りましょう!

なしば
なしば

改善案など大歓迎ですので、コメント頂けると嬉しいです!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です