はじめに
こんにちは、株式会社ユーザベース スピーダ事業 Sales System Engineering Teamの村松(あだ名:MJ)です。
ユーザベースのSalesforceのアドミン/デベロッパーを担当しています。
今回は私たちのチームで用いているトリガーフレームワークについてご紹介します!
Salesforce開発において、トリガーの管理は規模が大きくなるほど複雑になります。
オブジェクトごとにトリガーが増え、処理順序の制御やメンテナンスが困難になるというのは、
多くの開発チームが直面する課題かと思います。
私たちのチームでも、開発者ごとの設計でトリガー・トリガーフローが実装されており、
各機能の実行順の制御や再帰防止ができていませんでした。
実際に数種類の商品を商談に登録するだけで、再帰的もしくは不要なトリガーの実行などによってガバナ制限に抵触してしまい、
パフォーマンスに問題を抱えていました。
現状のままでは柔軟なシステム開発ができないため、
設計を統一し、トリガーの実行ロジックの管理をしやすくすることを目指してトリガーフレームワークを実装しました。
実装後、30種類の商品を商談に登録できるまでパフォーマンスが改善しています。
本記事では、カスタムメタデータ型を活用して、トリガーの実行ロジックを宣言的に管理できるフレームワークを解説します。
このフレームワークの特徴は以下の通りです。
- 1オブジェクト=1トリガーの原則を強制
- 実行するアクション(Apexクラス・フロー)をカスタムメタデータで管理
- 実行順序・有効/無効の制御をコードの変更なしで実現
- バイパス機構による柔軟な実行制御
アーキテクチャ概要

| コンポーネント | 役割 |
|---|---|
| BaseTrigger | フレームワークの中心。トリガーから呼ばれ、メタデータに基づいてアクションを実行する |
| BaseTriggerAction | アクションクラスが実装すべきインターフェースを定義する |
| TriggerSetting__mdt | オブジェクト単位のトリガー有効/無効を管理するカスタムメタデータ |
| TriggerActionSetting__mdt | 個々のアクション(ビジネスロジック)の設定を管理するカスタムメタデータ |
カスタムメタデータの設計
このフレームワークは2階層のカスタムメタデータで構成されています。
TriggerSetting__mdt(トリガー設定 親)
オブジェクト単位でトリガーの有効/無効を管理します。
| 項目 | 説明 | 例 |
|---|---|---|
SObjectName__c |
対象のオブジェクトAPI名 | Account |
IsActive__c |
トリガー全体の有効/無効 | true |
MaintenanceUsername__c |
メンテナンス中に除外するユーザー名 | admin@example.com |
TriggerActionSetting__mdt(トリガーアクション設定 子)
個々の処理(ビジネスロジック)を定義します。
| 項目 | 説明 | 例 |
|---|---|---|
TriggerSetting__c |
親のTriggerSetting__mdtへの参照 | (参照) |
OperationType__c |
トリガー操作タイプ | BEFORE_INSERT |
Type__c |
アクションの種類 | Apexクラス / フロー |
ActionName__c |
実行するApexクラス名またはフロー名 | AccountValidation |
Order__c |
実行順序 | 10 |
IsActive__c |
この個別アクションの有効/無効 | true |
MaintenanceUsername__c |
メンテナンス中に除外するユーザー名 | admin@example.com |
設定例のイメージ
| DeveloperName | OperationType | Type | ActionName | Order | IsActive |
|---|---|---|---|---|---|
| Account_Validate | BEFORE_INSERT | Apexクラス | AccountValidation | 10 | ✓ |
| Account_SetDefault | BEFORE_INSERT | Apexクラス | AccountDefaultSetter | 20 | ✓ |
| Account_Notify | AFTER_INSERT | フロー | Account_Notification_Flow | 10 | ✓ |
| Account_CalcScore | AFTER_UPDATE | Apexクラス | AccountScoreCalculator | 10 | ✗ |
BaseTriggerAction - インターフェース定義
BaseTriggerActionクラスは、各トリガー操作タイプに対応するインターフェースを内部クラスとして定義しています。
public class BaseTriggerAction { public interface BeforeInsert { Boolean valid(SObject triggerNew); void run(List<SObject> triggerNew); } public interface AfterInsert { Boolean valid(SObject triggerNew); void run(List<SObject> triggerNew); } ・ ・ ・ public interface AfterDelete { Boolean valid(SObject triggerOld); void run(List<SObject> triggerOld); } public interface AfterUndelete { Boolean valid(SObject triggerNew); void run(List<SObject> triggerNew); } }
すべてのインターフェースには2つのメソッドがあります。
| メソッド | 役割 |
|---|---|
valid() |
レコード単位で処理対象かどうかを判定する。trueを返したレコードだけがrun()に渡される |
run() |
実際のビジネスロジックを実行する。valid()で絞り込まれたレコードのリストを受け取る |
このフレームワークでは、valid() メソッドでレコード単位のフィルタリングを行い、条件を満たしたレコードだけがrun() に渡されます。
BaseTrigger - フレームワークのコア
trigger AccountTrigger on Account (
before insert, after insert,
before update, after update,
before delete, after delete,
after undelete
) {
new BaseTrigger().run();
}
各オブジェクトのトリガーは、上記のように1行だけで記述します。
run() メソッドが呼ばれると、同一トランザクション内ですでに実行済みでないこと、バイパス対象でないことを判定します。
その後、判定マークを付与し、OperationTypeに応じて、Apexクラスもしくはフローを呼び出します。
最後に実行中マークを解除して実行終了します。
Apexクラスの動的実行
フレームワークの核となる仕組みが、動的型解決によるApexクラスの実行です。
Type.forName()を使用することで直接クラスを参照せずに、文字列(クラス名)からクラスをインスタンス化しています。
これにより、新しいアクションを追加する際にフレームワーク自体のコードを変更する必要がありません。
// カスタムメタデータに登録されたクラス名から型を取得 Type t = Type.forName(triggerActionSetting.ActionName__c); // インターフェースにキャストしてインスタンス化 BaseTriggerAction.BeforeInsert action = (BaseTriggerAction.BeforeInsert) t.newInstance();
フローの動的実行
Apexクラスだけでなく、Salesforceフローもアクションとして登録できます。Invocable.Actionを使用して動的にフローを呼び出します。
Invocable.Action action = Invocable.Action.createCustomAction('Flow', actionName);
List<Map<String, Object>> params にnewRecordとoldRecordのマップを格納して、
setInvocations()で全レコード分のパラメータを一括でセットし、実行します。
action.setInvocations(params); action.invoke();
フローにはnewRecordとoldRecordがパラメータとして渡されるため、フロー側でも変更前後の値を比較したロジックを組むことが可能です。
再帰防止メカニズム
トリガーの再帰実行は、Salesforce開発でよくある問題であり、
私たちの環境でも再帰実行によるパフォーマンスの低下が問題になっていました。
このフレームワークでは、下記のstatic変数を使った同一トランザクション内の再帰防止が組み込まれています。
static Set<String> executings = new Set<String>();
動作の仕組み
- run()が呼ばれると、まず
valid()でexecutingsにオブジェクト名が含まれていないかをチェック - 含まれていなければ
executingsにオブジェクト名を追加して処理を開始 - 処理が完了したら
executingsからオブジェクト名を削除
例えば「Account トリガー → Contact 更新 → Contact トリガー → Account 更新 → Account トリガー(2回目)」のような循環参照が発生した場合、2回目の Account トリガーはexecutingsにAccountが含まれているためスキップされます。
バイパス機能
特定のアクションやオブジェクトのトリガーを一時的にスキップするバイパス機能を作成しています。
例えば、下記のようにバイパスする特定のアクションを指定してからInsertを行うことで、トリガーアクションをスキップすることが可能です。
BaseTrigger.bypass('AccountNotification'); insert accounts; BaseTrigger.clearBypass('AccountNotification');
メンテナンスモード
カスタムメタデータのMaintenanceUsername__c項目を使って、特定ユーザーに対してトリガーアクションをスキップさせることができます。
TriggerSetting__mdt(オブジェクトレベル) に設定すると、そのオブジェクトの全アクションがスキップされます。
TriggerActionSetting__mdt(アクションレベル) に設定すると、特定のアクションだけがスキップされます。
本番環境でのデータメンテナンス作業や、管理者によるメンテナンス操作時にとても便利です。
デプロイやコード変更なしに、メタデータの設定変更だけで特定ユーザーのトリガーをスキップできます。(他のユーザによる操作はトリガーが発火するので、業務時間中にもデータメンテナンスができる!!)
フレームワークの利点まとめ
実行するアクションの削除・順序の変更・有効/無効・メンテナンスモードの切り替えがすべてカスタムメタデータの設定で完結します。
新しい処理を追加する場合も、アクションクラスを作成し、カスタムメタデータにレコードを追加するだけで、既存のトリガーファイルを変更する必要がありません。
また、再帰防止がBaseTriggerにあるため、個別で実装する必要がなく、バイパス機能により柔軟にトリガーの実行を制御できます。
トリガーは一度動き始めると、機能追加のたびに“暗黙のルール”が増えがちです。
私たちのチームではその状態を避けるために、実行ロジックをメタデータで見える化し、運用で切り替えられる形に寄せました。
結果として、開発者が増えてもトリガーの理解コストを抑えつつ、変更に強い構成にできたと感じています。