執筆者:Salesforce 開発者 (Salesforce Developer)
Salesforceプラットフォームにおける開発者として、私たちは日々、ビジネス要件を自動化し、データの整合性を維持するための強力なツールを扱っています。その中でも、Apex Trigger(Apexトリガー)は、データ操作(DML)イベントに応じてカスタムロジックを実行するための最も基本的かつ強力なメカニズムです。本記事では、開発者の視点から、Apexトリガーの基礎から、スケーラブルで保守性の高いソリューションを構築するためのベストプラクティスまでを深く掘り下げていきます。
背景と適用シナリオ
Salesforceには、Flowやプロセスビルダーなど、多くの宣言的な自動化ツールが存在します。では、なぜ私たちはコードベースのアプローチであるApexトリガーを必要とするのでしょうか?
Apexトリガーは、以下のような複雑なシナリオでその真価を発揮します。
- 複雑なビジネスロジック: 複数のオブジェクトにまたがるレコードの作成や更新、宣言的ツールでは実現不可能な複雑な条件分岐など。
- 高度なデータ検証: データベースに保存される前に、関連レコードの値に基づいてレコードを検証する必要がある場合。例えば、「商談のフェーズが『受注』に変更された場合、関連するすべての商談商品に特定の値が設定されていなければならない」といったルールです。
- 外部システム連携: レコードが作成または更新されたタイミングで、外部システムのAPIを呼び出す必要がある場合(ただし、非同期処理を考慮する必要があります)。
- パフォーマンスの最適化: 大量データの一括処理(Bulkification)を前提とした、高度に最適化されたロジックを実装する必要がある場合。
開発者として、私たちは「いつトリガーを使うべきか」を正確に判断し、保守性、拡張性、そしてパフォーマンスを念頭に置いて設計・実装する責任があります。
原理の説明
Apexトリガーは、特定のオブジェクトでレコードが挿入、更新、削除、または復元される前(Before)または後(After)に実行されるApexコードのブロックです。
トリガーの構文
基本的な構文は以下の通りです。
trigger TriggerName on ObjectName (trigger_event, ...) {
// Code block
}
- TriggerName: トリガーの一意な名前。
- ObjectName: トリガーが関連付けられるsObjectの名前(例:Account、Contact)。
- trigger_event: トリガーを起動させるDMLイベント。コンマ区切りで複数指定可能です。
before insert: レコードがデータベースに保存される前。before update: レコードがデータベースで更新される前。before delete: レコードがデータベースから削除される前。after insert: レコードがデータベースに保存された後。after update: レコードがデータベースで更新された後。after delete: レコードがデータベースから削除された後。after undelete: レコードがごみ箱から復元された後。
コンテキスト変数 (Context Variables)
トリガーの実行中、Apexは実行コンテキストに関する情報を提供する静的変数(トリガーコンテキスト変数)を公開します。これらを活用することで、トリガーがどのような状況で起動されたかを判断し、効率的なロジックを記述できます。
- Trigger.new: 挿入または更新された新しいバージョンのsObjectレコードのリスト。
before insert、before update、after insert、after updateトリガーで利用可能です。beforeトリガーでは、このリスト内のレコードの値を変更することができます。 - Trigger.old: 更新または削除される前の古いバージョンのsObjectレコードのリスト。
updateおよびdeleteトリガーでのみ利用可能です。 - Trigger.newMap: レコードIDをキー、新しいバージョンのsObjectレコードを値とするMap。
after insert、before update、after updateトリガーで利用可能です。 - Trigger.oldMap: レコードIDをキー、古いバージョンのsObjectレコードを値とするMap。
updateおよびdeleteトリガーで利用可能です。 - Trigger.isExecuting: 現在のApexコードがトリガーのコンテキストで実行されている場合に
trueを返します。 - Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete, etc.: 現在のDML操作に対応するコンテキストで
trueを返します。 - Trigger.size: トリガーを起動したレコードの総数。
実行の順序 (Order of Execution)
トリガーは、Salesforceがレコードを保存する際の広範なプロセスのほんの一部です。開発者は、トリガーがどのタイミングで実行されるかを正確に理解する必要があります。例えば、beforeトリガーは、入力規則(Validation Rules)の前に実行され、afterトリガーは、レコードがデータベースにコミットされた後に実行されます。この順序を理解していないと、意図しない動作やデバッグの困難につながります。
示例代码
ベストプラクティスとして、「1オブジェクト1トリガー」の原則と、ロジックをトリガー本体から分離する「トリガーハンドラーパターン」を強く推奨します。これにより、コードの再利用性、保守性、テストの容易性が飛躍的に向上します。
例:取引先トリガーとハンドラークラス
ここでは、取引先(Account)オブジェクトに対するすべてのロジックを単一のトリガーと、それを処理するハンドラークラスに集約する例を示します。
1. トリガー本体 (AccountTrigger.trigger)
トリガー本体は、どのイベントでどのハンドラーメソッドを呼び出すかを決定する「ディスパッチャー(振り分け役)」に徹します。ロジックは一切含みません。
/*
* developer.salesforce.com のベストプラクティスに基づき、
* このトリガーはロジックを持たず、ハンドラークラスに処理を委譲します。
* これにより、コードの管理、テスト、再利用が容易になります。
* 参照: Apex Developer Guide - "Trigger and Bulk Request Best Practices"
*/
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
// 新しいハンドラーインスタンスを作成
AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.new, Trigger.oldMap);
// DMLイベントのコンテキストをチェックし、適切なメソッドを呼び出す
if (Trigger.isBefore) {
if (Trigger.isInsert) {
handler.onBeforeInsert();
} else if (Trigger.isUpdate) {
handler.onBeforeUpdate(Trigger.newMap, Trigger.oldMap);
} else if (Trigger.isDelete) {
handler.onBeforeDelete(Trigger.oldMap);
}
} else if (Trigger.isAfter) {
if (Trigger.isInsert) {
handler.onAfterInsert(Trigger.newMap);
} else if (Trigger.isUpdate) {
handler.onAfterUpdate(Trigger.newMap, Trigger.oldMap);
} else if (Trigger.isDelete) {
handler.onAfterDelete(Trigger.oldMap);
} else if (Trigger.isUndelete) {
handler.onAfterUndelete(Trigger.newMap);
}
}
}
2. ハンドラークラス (AccountTriggerHandler.cls)
実際のビジネスロジックはこのクラスに実装します。メソッドはDMLイベントごとに分割されており、非常に見通しが良くなります。
/*
* developer.salesforce.com のトリガーハンドラーパターンの概念に基づき、
* ビジネスロジックをカプセル化するクラス。
* 参照: Trailhead - "Apex Triggers" module, which promotes logic-less triggers.
*/
public class AccountTriggerHandler {
private List<Account> newRecords;
private Map<Id, Account> oldMap;
// コンストラクタでコンテキスト変数を初期化
public AccountTriggerHandler(List<Account> newRecords, Map<Id, Account> oldMap) {
this.newRecords = newRecords;
this.oldMap = oldMap;
}
// before insert イベントのロジック
public void onBeforeInsert() {
// 例: 新規作成される取引先の「説明」項目にデフォルト値を設定
for (Account acct : newRecords) {
if (String.isBlank(acct.Description)) {
acct.Description = 'This account was created via trigger.';
}
}
}
// before update イベントのロジック
public void onBeforeUpdate(Map<Id, Account> newMap, Map<Id, Account> oldMap) {
// 例: 取引先の評価(Rating)が 'Hot' に変更された場合、特定の操作を禁止
for (Account newAcct : newMap.values()) {
Account oldAcct = oldMap.get(newAcct.Id);
if (newAcct.Rating == 'Hot' && oldAcct.Rating != 'Hot') {
if (newAcct.AnnualRevenue == null || newAcct.AnnualRevenue < 1000000) {
// addError() メソッドでDML操作を中止し、エラーメッセージを表示
newAcct.addError('Rating cannot be set to Hot for accounts with annual revenue less than $1M.');
}
}
}
}
// after insert イベントのロジック
public void onAfterInsert(Map<Id, Account> newMap) {
// 例: 新規取引先が作成されたら、関連する歓迎タスクを作成
List<Task> tasksToInsert = new List<Task>();
for (Account acct : newMap.values()) {
// Bulkificationを意識し、ループ内でDMLは行わない
tasksToInsert.add(new Task(
Subject = 'Follow up with new account: ' + acct.Name,
WhatId = acct.Id
));
}
if (!tasksToInsert.isEmpty()) {
// ループの外で一度だけDMLを実行
insert tasksToInsert;
}
}
// 他のイベント(onBeforeDelete, onAfterUpdateなど)のメソッドも同様に定義...
public void onAfterUpdate(Map<Id, Account> newMap, Map<Id, Account> oldMap) { /* ... */ }
public void onBeforeDelete(Map<Id, Account> oldMap) { /* ... */ }
public void onAfterDelete(Map<Id, Account> oldMap) { /* ... */ }
public void onAfterUndelete(Map<Id, Account> newMap) { /* ... */ }
}
注意事項
Apexトリガーを実装する際には、Salesforceプラットフォームの制約と特性を深く理解しておく必要があります。
一括処理 (Bulkification)
Salesforceでは、データローダーやAPIを通じて一度に最大200件のレコードが処理される可能性があります。トリガーは、この「一括処理」を前提として設計されなければなりません。絶対にループの中でSOQLクエリやDMLステートメントを実行してはいけません。これは、Governor Limits(ガバナ制限)に抵触する最も一般的な原因です。
悪い例:
for (Account a : Trigger.new) {
// ループ内でクエリを実行しており、レコード数分のクエリが発行されてしまう
Contact c = [SELECT Id FROM Contact WHERE AccountId = :a.Id LIMIT 1];
// ...
}
良い例:
Set<Id> accountIds = Trigger.newMap.keySet(); // ループの外で一度だけクエリを実行 List<Contact> relatedContacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]; // ...
ガバナ制限 (Governor Limits)
Salesforceは、マルチテナント環境の安定性を保つため、1回のトランザクションで使用できるリソース(SOQLクエリの発行回数、DML操作の回数、CPU時間など)に厳格な制限を設けています。トリガーの実装は、常にこれらの制限を意識し、効率的なコードを心がける必要があります。
再帰 (Recursion)
トリガー内のロジックが、トリガー自身を再度起動させるレコード更新を行うと、無限ループ(再帰)が発生する可能性があります。例えば、取引先の更新トリガーが、同じ取引先の項目を更新するようなケースです。これを防ぐためには、静的変数を用いた再帰制御パターンを実装するのが一般的です。
public class MyTriggerHandler {
private static boolean hasAlreadyRun = false;
public static void execute() {
if (!hasAlreadyRun) {
hasAlreadyRun = true;
// ここに一度だけ実行したいロジックを記述
// ...
}
}
}
エラー処理
beforeトリガーでデータの保存を中止させたい場合は、sObject.addError('エラーメッセージ')メソッドを使用します。これにより、トランザクションがロールバックされ、ユーザーに明確なフィードバックが提供されます。afterトリガーで発生した予期せぬ例外は、try-catchブロックで捕捉し、適切にログを記録するなどの処理が必要です。
まとめとベストプラクティス
高品質でスケーラブルなApexトリガーを開発するためには、以下のベストプラクティスを遵守することが不可欠です。
- 1オブジェクト1トリガー (One Trigger Per Object):
1つのオブジェクトに対してトリガーは1つだけ作成します。これにより、複数のトリガーがどの順序で実行されるかを心配する必要がなくなり、実行順序を完全に制御できます。
- ロジックレスなトリガー (Logic-less Triggers):
トリガー本体にはビジネスロジックを記述せず、ハンドラークラスに処理を委譲します。これにより、コードのモジュール化が進み、テストや再利用が格段に容易になります。
- 一括処理を前提とした設計 (Bulkify Your Code):
コードは常に複数のレコードを処理できるように設計します。ループ内でのSOQLやDMLは厳禁です。
- コンテキスト固有のメソッド (Context-Specific Handler Methods):
ハンドラークラス内では、
onBeforeInsert、onAfterUpdateのように、トリガーのコンテキストごとにメソッドを分割します。これにより、コードの可読性と保守性が向上します。 - 再帰制御の実装 (Avoid Recursion):
静的変数などを用いて、トリガーが意図せず再帰的に実行されるのを防ぎます。
- 十分なテストカバレッジ (Comprehensive Test Coverage):
トリガーのロジックは、本番環境にデプロイするために最低75%のコードカバレッジが必要です。しかし、単にカバレッジを満たすだけでなく、単一レコード、複数レコード(一括処理)、境界値、エラーケースなど、あらゆるシナリオを網羅した堅牢なテストクラスを作成することが重要です。
Apexトリガーは、Salesforceプラットフォームのカスタマイズにおける強力な武器です。しかし、その力は大きな責任を伴います。本記事で紹介した原理とベストプラクティスを深く理解し、常にプラットフォームのパフォーマンスと健全性を考慮した開発を心がけることで、私たちはビジネスに真の価値を提供する、信頼性の高いソリューションを構築できるのです。
コメント
コメントを投稿