一括 Apex トリガー
学習の目的
この単元を完了すると、次のことができるようになります。
- sObject のコレクションに対して動作するトリガーを作成する。
- 効率の良い SOQL 操作と DML 操作を実行するトリガーを作成する。
一括トリガーの設計パターン
Apex トリガーは、一括処理を行うように最適化されています。トリガーのレコード処理には一括設計パターンの使用をお勧めします。一括設計パターンを使用すると、トリガーのパフォーマンスが向上し、サーバーリソースの消費が抑えられ、プラットフォームの制限を超える可能性が低くなります。
コードを一括処理化する利点は、一括処理化されたコードによって多数のレコードを効率的に処理でき、Lightning Platform のガバナ制限内でコードを実行できる点です。こうしたガバナ制限を設定する目的は、回避コードがマルチテナントプラットフォームのリソースを占有しないようにすることです。
次のセクションでは、トリガーの Apex コードを一括処理化する主な方法について説明します。コードを一括処理化すると、トリガーのすべてのレコードが処理され、SOQL や DML が一度に 1 つの sObject ではなく、sObject のコレクションに対して実行されます。SOQL および DML の一括操作のベストプラクティスは、クラスの SOQL や DML をはじめとする Apex コードに適用されることです。次の例では、トリガーをベースに、Trigger.new
コンテキスト変数を使用します。
レコードセットに対する処理
まず、トリガーの一括設計のごく基本的な概念から見ていきましょう。一括処理化されたトリガーは、トリガーコンテキストのすべての sObject に対して実行されます。通常、トリガーを実行したアクションがユーザーインターフェースで作成されたものである場合は、1 つのレコードに対してトリガーが実行されます。ただし、アクションが一括 DML または API で作成されたものである場合、1 つのレコードではなく、レコードセットに対してトリガーが実行されます。たとえば、API を介して多くのレコードをインポートした場合、トリガーはそのレコードセット全体に対して実行されます。したがって、常にトリガーがレコードのコレクションに対して実行されることを想定してプログラミングを行い、トリガーがどの状況でも機能するようにしておきます。
次のトリガー (MyTriggerNotBulk
) は、1 つのレコードのみによってトリガーが実行されたことを想定しています。同一のトランザクションで複数のレコードが挿入された場合、このトリガーはレコードセット全体に対して実行されません。次の例は、一括処理化されたバージョンを示します。
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.new[0]; a.Description = 'New description'; }
次の例 (MyTriggerBulk
) は、MyTriggerNotBulk
を変更したバージョンです。この場合は、for
ループを使用して利用可能なすべての sObject に反復処理を行います。このループは、Trigger.new
に 1 つの sObject または複数の sObject がある場合に機能します。
trigger MyTriggerBulk on Account(before insert) { for(Account a : Trigger.new) { a.Description = 'New description'; } }
一括 SOQL の実行
SOQL クエリは非常に高機能です。1 つのクエリで関連レコードを取得し、複数の条件の組み合わせをチェックできます。SOQL 機能を使用することで、記述するコードの量やデータベースクエリの数を減らすことができます。データベースクエリの数を減らすことで、クエリ制限 (同期 Apex の場合は 100、非同期 Apex の場合は 200 の SOQL クエリ) に達するのを回避しやすくなります。
次のトリガー (SoqlTriggerNotBulk
) は、避けるべき SOQL クエリのパターンを示しています。この例は、for
ループ内に SOQL クエリを作成して取引先ごとに関連する商談を取得するもので、Trigger.new
の Account sObject ごとに 1 回ずつクエリが実行されます。多くの取引先が含まれるリストがある場合、for
ループ内部の SOQL クエリによって、SOQL クエリの数が多くなり、制限に達する可能性があります。次の例は、推奨されるアプローチを示します。
trigger SoqlTriggerNotBulk on Account(after update) { for(Account a : Trigger.new) { // Get child records for each account // Inefficient SOQL query as it runs once for each account! Opportunity[] opps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId=:a.Id]; // Do some other processing } }
次の例 (SoqlTriggerBulk
) は前の例を変更したバージョンであり、SOQL クエリの実行のベストプラクティスを示しています。SOQL クエリが面倒な作業を行い、クエリがメインループ外で 1 回コールされます。
- SOQL クエリは、内部クエリ (
SELECT Id FROM Opportunities
) を使用して取引先の関連商談を取得します。 IN
句を使用すること、およびWHERE
句WHERE Id IN :Trigger.new
にTrigger.new
変数をバインドすることによって、SOQL クエリがトリガーコンテキストのレコードと結び付けられます。このWHERE
条件により、トリガーを実行した取引先レコードのみに絞り込まれます。
クエリ内で 2 つの部分が組み合わされると、必要とするレコード、つまり、このトリガーの取引先と各取引先の関連商談が 1 回のコールで取得されます。
レコードとその関連レコードが取得されたら、for
ループは、コレクション変数 (ここでは acctsWithOpps
) を使用して、関心のあるレコードに反復処理を行います。SOQL クエリの結果はこのコレクション変数に保持されます。この方法では、for
ループが関心のあるレコードのみに反復処理を行います。関連レコードはすでに取得されているため、ループ内にこれらのレコードを取得するためのさらなるクエリは必要ありません。
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the accounts and their related opportunities. List<Account> acctsWithOpps = [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities) FROM Account WHERE Id IN :Trigger.new]; // Iterate over the returned accounts for(Account a : acctsWithOpps) { Opportunity[] relatedOpps = a.Opportunities; // Do some other processing } }
または、親の取引先レコードが必要ない場合は、このトリガーコンテキスト内の取引先に関連する商談のみを取得できます。このリストは、Trigger.new
の WHERE
句で商談の AccountId
項目と取引先の ID
を照合して指定されます (WHERE AccountId IN :Trigger.new
)。返された商談は、このトリガーコンテキストのすべての取引先に対するもので、特定の取引先に対するものではありません。次の例は、関連するすべての商談を取得するために使用するクエリを示します。
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Do some other processing } }
上記の例については、SOQL クエリと for
ループを SOQL for
ループという 1 つのステートメントにまとめれば、コードを短くすることができます。次の例は、上記の一括トリガーに SOQL for
ループを使用した別バージョンです。
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger, // and iterate over those records. for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]) { // Do some other processing } }
一括 DML の実行
トリガーまたはクラスで DML コールを実行する場合、可能ならば sObject のコレクションに DML コールを実行します。各 sObject に DML を個別に実行すると、リソースの使用が非効率的です。Apex ランタイムでは、1 回のトランザクションで最大 150 の DML コールを実行できます。
このトリガー (DmlTriggerNotBulk
) は、関連する商談に対して反復処理を行う for
ループ内で update コールを実行します。特定の条件に一致すると、トリガーは商談の説明を更新します。この例では、update ステートメントが商談ごとに 1 回ずつコールされるため、非効率的です。取引先の一括更新操作でトリガーを実行する場合、取引先が数多く存在する可能性があります。各取引先に 1 ~ 2 件の商談があれば、商談数がたちまち 150 を超えてしまいます。DML ステートメントの制限は 150 コールです。
trigger DmlTriggerNotBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; // Update once for each opportunity -- not efficient! update opp; } } }
次の例 (DmlTriggerBulk
) は、商談のリストに対する 1 回の DML コールで DML を一括して実行する効率的な方法を示しています。この例では、更新する Opportunity sObject をループの商談リスト (oppsToUpdate
) に追加します。すべての商談がリストに追加された後、トリガーがループ外でこのリストに DML コールを実行します。このパターンでは、更新される sObject の数に関係なく、DML コールが 1 回だけ実行されます。
trigger DmlTriggerBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; List<Opportunity> oppsToUpdate = new List<Opportunity>(); // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; oppsToUpdate.add(opp); } } // Perform DML on a collection update oppsToUpdate; }
一括設計パターンの動作: 関連レコードを取得する例
取引先の関連する商談にアクセスするトリガーを記述して、上述の設計パターンを適用してみましょう。前の単元の AddRelatedRecord
トリガーの例を変更します。AddRelatedRecord
トリガーは一括操作されますが、前の単元の場合はすべての Trigger.new
sObject レコードに反復処理が行われるため、最も効率的というわけではありません。次の例では、関心のあるレコードのみを取得し、それらのレコードに反復処理を行うようにトリガーコードと SOQL クエリの両方を変更します。このトリガーを作成していなくても心配いりません。このセクションで作成できます。
AddRelatedRecord
トリガーの要件から確認していきましょう。取引先が挿入または更新されるとこのトリガーが実行されます。このトリガーによって、まだ商談がない各取引先にデフォルトの商談が追加されます。
まず、新しく挿入された取引先にはデフォルトの商談がないため、追加する必要があります。一方、更新される取引先の場合は関連商談があるかどうかを調べる必要があります。そこで、Trigger.operationType
コンテキスト変数に対する switch
ステートメントを使用して挿入と更新の処理を分けます。次に、処理する必要がある取引先を toProcess
変数で追跡します。次に例を示します。
List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // do stuff } when AFTER_UPDATE { // do stuff } }
すべての取引先の挿入では、新しい取引先を toProcess
リストに割り当てるだけです。
when AFTER_INSERT { toProcess = Trigger.New; }
更新の場合は、このトリガーでまだ関連商談のないはどれかを解明する必要があります。このトリガーは after トリガーであるため、影響を受けるレコードをデータベースから照会できます。SOQL ステートメントは次のようになり、その結果を toProcess
リストに割り当てます。
when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; }
次は、for
ループを使用して取引先の toProcess
リストを反復処理し、関連するデフォルト商談を oppList
に追加します。終わったら、insert
DML ステートメントを使用して商談のリストを一括追加します。 次の手順に従って完全なトリガーを作成または更新します。
- 前の単元で
AddRelatedRecord
トリガーを作成している場合は、その内容を次のトリガーと置き換えて変更します。作成していない場合は、開発者コンソールを使用して次のトリガーを追加し、トリガー名にAddRelatedRecord
と入力します。trigger AddRelatedRecord on Account(after insert, after update) { List<Opportunity> oppList = new List<Opportunity>(); // Add an opportunity for each account if it doesn't already have one. // Iterate over accounts that are in this trigger but that don't have opportunities. List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // All inserted Accounts will need the Opportunity, so there is no need to perform the query toProcess = Trigger.New; } when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; } } for (Account a : toProcess) { // Add a default opportunity for this account oppList.add(new Opportunity(Name=a.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=a.Id)); } if (oppList.size() > 0) { insert oppList; } }
- このトリガーをテストするには、Salesforce ユーザーインターフェースで取引先を作成して、
Lions & Cats
という名前を付けます。 - 取引先のページの [商談] 関連リストで、新しい商談
Lions & Cats
を見つけます。トリガーによって商談が自動的に追加されています。
リソース