一括 Apex トリガ
一括トリガの設計パターン
コードを一括処理化する利点は、一括処理化されたコードによって多数のレコードを効率的に処理でき、Lightning Platform のガバナ制限内でコードを実行できる点です。こうしたガバナ制限を設定する目的は、回避コードがマルチテナントプラットフォームのリソースを占有しないようにすることです。
次のセクションでは、トリガの Apex コードを一括処理化する主な方法について説明します。コードを一括処理化すると、トリガのすべてのレコードが処理され、SOQL や DML が一度に 1 つの sObject ではなく、sObject のコレクションに実行されます。SOQL および DML の一括操作のベストプラクティスは、クラスの SOQL や DML をはじめとする Apex コードに適用されることです。次の例では、トリガをべースに、Trigger.New コンテキスト変数を使用します。
レコードセットに対する処理
まず、トリガの一括設計のごく基本的な概念から見ていきましょう。一括処理化されたトリガは、トリガコンテキストのすべての sObject に対して実行されます。通常、トリガを実行したアクションがユーザインターフェースで作成されたものである場合は、1 つのレコードに対してトリガが実行されます。ただし、アクションが一括 DML または API で作成されたものである場合、1 つのレコードではなく、レコードセットに対してトリガが実行されます。たとえば、API を介して多くのレコードをインポートした場合、トリガはそのレコードセット全体に対して実行されます。したがって、常にトリガがレコードのコレクションに対して実行されることを想定してプログラミングを行い、トリガがどの状況でも機能するようにしておきます。
次のトリガは、1 つのレコードのみによってトリガが実行されたことを想定しています。同一のトランザクションで複数のレコードが挿入された場合、このトリガはレコードセット全体に対して実行されません。次の例は、一括処理化されたバージョンを示します。
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.New[0]; a.Description = 'New description'; }
次の例は、MyTrigger を変更したものです。この場合は、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 クエリ) に達するのを回避しやすくなります。
次のトリガは、避けるべき 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 } }
次の例は前の例を変更したもので、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 } }
中級編
トリガは、一度に 200 レコードのバッチに対して実行されます。つまり、400 レコードでトリガが実行された場合、1 回につき 200 レコードずつ、トリガが 2 回実行されます。したがって、トリガの SOQL for ループのレコードをバッチにまとめるメリットがありません。これは、トリガでもレコードがバッチにまとめられるためです。この例では SOQL for ループは 2 回コールされますが、単独の SOQL クエリの場合でも 2 回コールされます。ただし、コレクション変数を反復処理するよりも、SOQL for ループを使用したほうが簡潔で明解です。
一括 DML の実行
トリガまたはクラス で DML コールを実行する場合、可能ならば sObject のコレクションに DML コールを実行します。各 sObject に DML を個別に実行すると、リソースの使用が非効率的です。Apex ランタイムでは、1 回のトランザクションで最大 150 の DML コールを実行できます。
このトリガは、関連する商談に対して反復処理を行う 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; } } }
次の例は、商談のリストに対する 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 トリガの要件から見ていきましょう。取引先が挿入または更新されるとこのトリガが実行されます。このトリガによって、まだ商談がない各取引先にデフォルトの商談が追加されます。最初に取り組むべき問題は、子の商談レコードを取得する方法を見つけることです。このトリガは after トリガであるため、影響を受けるレコードをデータベースから照会できます。これらのレコードは、after トリガが起動されるときにはすでにコミットされています。このトリガで関連する商談がないすべての取引先を返す SOQL クエリを記述してみましょう。
[SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity)]
これで関心のあるレコードのサブセットが取得されたため、次のとおり、SOQL for ループを使用してこれらのレコードに反復処理を行います。
for(Account a : [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity)]){ }
これでトリガの基本を確認できました。唯一欠けている部分は、デフォルトの商談を作成することです。次に一括して作成します。完全なトリガは次のとおりです。
- 前の単元で 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. for (Account a : [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity)]) { // 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」を見つけます。トリガによって商談が自動的に追加されています。