背景と応用シナリオ
Salesforce アーキテクトとして、我々はプラットフォームの健全性、拡張性、保守性を長期的に維持する責任を負っています。Salesforce の自動化ツールの中で、Apex Trigger (Apex トリガー) は最も強力でありながら、最も慎重な設計が求められる機能の一つです。フロー (Flow) や入力規則 (Validation Rule) といった宣言的なツールでは実現不可能な、複雑なビジネスロジックを実装するために使用されます。
Apex トリガーは、レコードがデータベースに挿入、更新、削除されるといった特定のデータ操作言語 (Data Manipulation Language, DML) イベントをきっかけに、カスタムの Apex コードを自動実行する仕組みです。アーキテクトの視点から見た主な応用シナリオは以下の通りです。
複雑なデータ検証
複数の関連オブジェクトにまたがる、あるいは複雑な計算を必要とする検証ロジックを実装します。例えば、「商談が『成立』になる際、関連する全ての納品先住所が有効でなければならない」といったルールです。
関連レコードの自動操作
特定の条件を満たした場合に、関連する親レコードや子レコードを自動的に作成、更新、または削除します。例えば、取引先 (Account) の種別が「パートナー」に変更された際、自動的にパートナー契約レコードを作成するようなケースです。
非正規化とデータ同期
パフォーマンス向上のために、関連オブジェクトの情報を親オブジェクトの項目に集約・コピー(非正規化)します。また、外部システムとの連携において、Salesforce 内のデータ変更をトリガーに、リアルタイムで外部システムへデータを送信する際にも利用されます。
ガバナンスとセキュリティの強制
標準の共有設定では実現できない、動的かつ複雑なレコード共有ルールを実装したり、特定のプロファイルによるレコードの削除を条件付きで禁止したりするなど、組織独自のガバナンスポリシーをコードレベルで強制します。
原理説明
Apex トリガーの動作を理解することは、スケーラブルなソリューションを設計する上で不可欠です。トリガーは、特定のオブジェクトに関連付けられ、DML イベントの前 (before) または後 (after) に実行されます。
トリガーの種別
- Before Triggers (before トリガー): レコードがデータベースに保存される「前」に実行されます。主な用途は、レコードの値の検証や変更です。このコンテキストでは、
Trigger.newに含まれるレコードの項目値を直接変更でき、追加の DML 操作なしで変更をデータベースに反映させることができます。 - After Triggers (after トリガー): レコードがデータベースに保存された「後」に実行されます。この時点ではレコードに ID が割り当てられており、項目値は読み取り専用です。主な用途は、関連レコードの操作(作成や更新)、非同期処理(キュー可能 Apex や future メソッドの呼び出し)、Chatter 投稿など、元のレコードの保存が完了している必要がある処理です。
コンテキスト変数 (Context Variables)
トリガーの実行中、Apex は処理中のレコードに関する情報を提供する静的な変数、いわゆる「コンテキスト変数」を公開します。これらを適切に利用することが、効率的なトリガーを記述する鍵となります。
- Trigger.new: 処理対象の新しいバージョンのレコードリスト (
List<sObject>)。insert、update、undeleteトリガーで利用可能です。beforeトリガーでは項目値を変更できます。 - Trigger.old: 処理対象の古いバージョンのレコードリスト (
List<sObject>)。update、deleteトリガーで利用可能です。項目値は変更できません。 - Trigger.newMap: レコード ID をキーとし、新しいバージョンのレコードを値とするマップ (
Map<Id, sObject>)。update、delete、after insert、after undeleteトリガーで利用可能です。 - Trigger.oldMap: レコード ID をキーとし、古いバージョンのレコードを値とするマップ (
Map<Id, sObject>)。update、deleteトリガーで利用可能です。 - Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete, Trigger.isUndelete: 現在の DML 操作を判定する boolean 値。
- Trigger.isBefore, Trigger.isAfter: トリガーが before/after のどちらのコンテキストで実行されているかを判定する boolean 値。
アーキテクトは、これらの変数を活用して、トリガーが特定の条件下でのみロジックを実行するように制御し、コードの効率性と再利用性を高める必要があります。特に、大量のデータを扱う際には、Map 形式のコンテキスト変数(Trigger.newMap, Trigger.oldMap)がパフォーマンス最適化に極めて重要です。
サンプルコード
ここでは、Salesforce の公式ドキュメントで紹介されている典型的な例として、「関連する進行中の商談 (Opportunity) が存在する取引先 (Account) の削除を禁止する」トリガーを見てみましょう。これはビジネスの整合性を保つための一般的な要件です。
この例は before delete のコンテキストを使用します。レコードが削除される前に検証を行い、条件を満たさない場合は .addError() メソッドを使って DML 操作を中止させます。
trigger AccountDeletion on Account (before delete) {
// 削除対象の取引先に関連する商談をクエリする。
// Trigger.oldMap.keySet() を使用することで、一度の SOQL クエリで
// 全ての削除対象取引先に関連する商談を取得できる。(一括処理の原則)
Map<Id, Account> accountsWithOpps = new Map<Id, Account>([
SELECT Id, (SELECT Id FROM Opportunities)
FROM Account
WHERE Id IN :Trigger.oldMap.keySet()
]);
// 削除対象の各取引先をループ処理する
for (Account acc : Trigger.old) {
// accountsWithOpps マップから、現在処理中の取引先を取得する。
// この取引先には、サブリレーションクエリによって関連する商談のリストが含まれている。
Account accWithOpps = accountsWithOpps.get(acc.Id);
// 関連する商談が存在するかどうかをチェックする
if (accWithOpps.Opportunities != null && !accWithOpps.Opportunities.isEmpty()) {
// 商談が存在する場合、.addError() メソッドを使用して削除操作をブロックし、
// ユーザーに分かりやすいエラーメッセージを表示する。
// これによりトランザクション全体がロールバックされる。
acc.addError('Cannot delete account with related opportunities.');
}
}
}
このコードは、一括処理 (Bulkification) の原則に従って設計されています。トリガー内の for ループの中で SOQL クエリを実行するのではなく、一度のクエリで全ての関連データを取得しています。これは、Salesforce のガバナ制限 (Governor Limits) を回避するための絶対的な要件です。
注意事項
Apex トリガーを設計・実装する際には、プラットフォームの制約と潜在的なリスクを十分に理解しておく必要があります。
ガバナ制限 (Governor Limits)
Salesforce はマルチテナント環境であるため、共有リソースの過剰な消費を防ぐための厳格な実行制限(ガバナ制限)を設けています。トリガーは、1つのトランザクション内で実行される SOQL クエリの数(最大100回)、DML ステートメントの数(最大150回)、CPU 時間などに制約を受けます。これらの制限を超えるコードは、実行時エラーを引き起こします。アーキテクトは、常にあらゆるトリガーが200件のレコードを一括で処理できる設計(バルク対応)になっていることを保証しなければなりません。
再帰実行 (Recursion)
トリガーのロジックが、同じトリガーを再度起動させるような DML 操作を行うと、無限ループ(再帰実行)に陥る可能性があります。例えば、after update トリガー内でトリガー対象のレコードを再度更新するような場合です。これを防ぐため、静的 (static) な boolean 変数を使用して、特定のトランザクション内でトリガーが一度しか実行されないように制御するデザインパターンが一般的に用いられます。
public class MyTriggerHandler {
private static boolean hasAlreadyRun = false;
public static void execute() {
if (!hasAlreadyRun) {
hasAlreadyRun = true;
// ここにトリガーのロジックを記述
}
}
}
エラーハンドリングとトランザクション制御
.addError() メソッドは、DML 操作を安全に中止させ、ユーザーにフィードバックを提供する強力な手段です。しかし、外部システムへのコールアウトなど、部分的な失敗が許容されるべき処理も存在します。このような場合、try-catch ブロックを適切に使用して例外を捕捉し、ビジネス要件に応じて処理を継続するか、あるいはトランザクションを失敗させるかを決定する必要があります。トランザクションの境界を意識した設計が重要です。
セキュリティと実行コンテキスト
Apex トリガーは「システムモード (system mode)」で実行されます。これは、コードが実行ユーザーの項目レベルセキュリティ (Field-Level Security) やオブジェクト権限を無視して動作することを意味します。この特性は強力ですが、意図せずユーザーにアクセス権のないデータを操作・閲覧させてしまうリスクも伴います。アーキテクトは、WITH SECURITY_ENFORCED 句を SOQL クエリで使用するなど、ビジネス要件に応じてセキュリティをコードレベルで強制する設計を検討する必要があります。
まとめとベストプラクティス
Salesforce プラットフォームのアーキテクトとして、Apex トリガーは強力なツールであると同時に、システムの複雑性と技術的負債の源泉にもなり得ます。持続可能でスケーラブルなソリューションを構築するために、以下のベストプラクティスを組織の標準として徹底することが不可欠です。
1. 宣言的アプローチを最優先に (Declarative First)
コードを書く前に、必ずフロー、入力規則、積み上げ集計項目などの宣言的ツールで要件を満たせないか検討します。宣言的ツールは、一般的に保守が容易で、アップグレードにも強いです。Apex トリガーは、宣言的ツールでは実現できない、最後の手段としてのみ採用すべきです。
2. 1オブジェクトにつき1トリガー (One Trigger Per Object)
1つのオブジェクトに対して複数のトリガーを作成すると、実行順序を制御できなくなり、デバッグや保守が極めて困難になります。全てのロジックを1つのトリガーに集約し、そのトリガーからロジックを担うハンドラクラスを呼び出す構造にすることで、実行順序が保証され、管理が容易になります。
3. ロジックレスなトリガーとハンドラパターン (Logic-less Triggers and Handler Pattern)
トリガーファイル自体には、複雑なビジネスロジックを記述すべきではありません。トリガーの役割は、実行コンテキスト(例:before insert, after update)を判定し、処理を専門の Apex クラス(ハンドラクラス)に委譲することに限定します。これにより、以下の利点が生まれます。
- 再利用性: ハンドラのメソッドは、トリガー以外の場所(例:バッチ処理、Visualforce コントローラ)からも呼び出すことができます。
- テスト容易性: ビジネスロジックが独立したクラスにあるため、単体テストが非常に書きやすくなります。
- 可読性と保守性: コードの関心事が分離され、各クラスの役割が明確になります。
4. 常に一括処理を前提とする (Always Think in Bulk)
全てのトリガーロジックは、単一レコードだけでなく、最大200レコードのリストを処理できるように設計しなければなりません。for ループ内での SOQL や DML の発行は絶対に避け、Map や Set を活用してデータを効率的に処理する設計を徹底します。
5. 十分なテストカバレッジ (High Test Coverage)
Salesforce が要求する 75% のテストカバレッジは、デプロイのための最低条件に過ぎません。アーキテクトとしては、全てのビジネスロジック、正常系・異常系、単一レコード処理、一括処理のシナリオを網羅する、95% 以上のカバレッジを目指すべきです。質の高いテストは、将来のリグレッションを防ぎ、システムの安定性を保証します。
これらの原則に従うことで、我々アーキテクトは、Apex トリガーを単なる自動化ツールとしてではなく、長期間にわたって組織の成長を支える、堅牢で保守性の高いシステムを構築するための戦略的資産として活用することができるのです。
コメント
コメントを投稿