Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

6

Share

Download to read offline

ApexトリガのBest Practiceを目指して

Download to read offline

Apex Trigger
(Tokyo Salesforce Developer Group Meetup #18)

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

ApexトリガのBest Practiceを目指して

  1. 1. #sfdg Apex Triggerの ベストプラクティスを目指して(進捗作成中) [Tokyo] Salesforce DG Meetup #18 Takahiro Yonei (@yonet77)
  2. 2. #sfdg  米井 孝浩(よねい たかひろ)  TAOドライブ株式会社 エンジニア  Salesforce向けの受託開発をメインにしてます  (最近は、Herokuも少し...)  Salesforce DG (Tokyo) の運営メンバの1人  (いちおう)Salesforce MVP (Spring’ 15)
  3. 3. #sfdg 1. Apexトリガについて(ちょっとおさらいなど) 2. より良いApexトリガの実装を目指して (1) 3. より良いApexトリガの実装を目指して (2)
  4. 4. #sfdg
  5. 5. #sfdg  Salesforceレコードへの変更前後にカスタムロジックを実行する機能  以下の操作の前後に実行することが可能 • insert • update • delete • merge • upsert • undelete  データベースのトリガと大体似たような感じ -> Salesforceでは、それがApexで記述できるのが良い
  6. 6. #sfdg  だいたいこんな感じ trigger ContextExampleTrigger on Account (before insert, after insert, after delete) { if (Trigger.isInsert) { if (Trigger.isBefore) { // Before Insert で何か処理したいことなど } else if (Trigger.isAfter) { // After Insert で何か処理したいことなど } } else if (Trigger.isDelete) { // After Delete で何か処理したいことなど } }
  7. 7. #sfdg  実装上、注意する点など  トリガの実行順序 1. 元のレコードがロード、または初期化される 2. 新しいレコードのフィールド値がロードされ、古い値を上書きする 3. 全てのbeforeトリガが実行される 4. カスタム検証ルールを含むシステム検証が行われる 5. 重複ルールが実行される 6. レコードはデータベースに保存されるが、コミットはされない 7. すべてのafterトリガが実行される 8. 割り当てルールが実行される 9. 自動応答ルールが実行される 10. ワークフロールールが実行される 11. ワークフローフィールドが更新されたら、レコードがリロードされる 12. 10の処理をうけて、beforeトリガとafterトリガを再度1度だけ実行する 13. エスカレーションルールが実行される 14. レコードが積み上げ集計項目をもっていたりクロスオブジェクトワークフローの一部である場合は、 親レコードの該当項目の値も更新する 15. すべてのDML操作がデータベースにコミットされる 16. 電子メールの送信など、コミット後のロジックが実行される 入力規則の前に beforeトリガが実行される ワークフロールールは afterトリガの後に実行される
  8. 8. #sfdg  実装上、注意する点など  1つのオブジェクトに複数のトリガが定義されている場合、 トリガの実行順序は制御できない • 例えば、sandbox環境での順序と、運用環境での順序は異なる • 1オブジェクト -> 1トリガ が基本  トリガを呼び出さない操作には以下がある(詳細は公式ドキュメントを) • 削除のカスケード • マージ操作の結果として親が変更される子レコードの更新カスケード • キャンペーン状況、住所の一括変更 • 選択リストの名前変更 or 置換 ...etc
  9. 9. #sfdg  実装上、注意する点など  一括操作を前提としてロジックを組むことが必須 • コレクション(Listなど)にレコードを追加し、それに対してDMLを実行 することで、DMLステートメント数を最小限にする • レコードを事前処理してコレクションを生成し、SOQLステートメントに 組み込むことで、SOQLステートメント数を最小限にする etc
  10. 10. #sfdg
  11. 11. #sfdg  Apexトリガテンプレートを利用する  A Simple Trigger Template for Salesforce を参考にしてみる  このテンプレートは、以下の課題をクリアすることを目指したもの • バルク処理が考慮されたテンプレートになってない • 7つのBoolean型のコンテキスト変数が、可読性とメンテナンス性の維持を 妨げている • 特定のコンテキストでは、Trigger.old, Trigger.new が使えないのを忘れがち • 非同期処理は、毎回自前で考慮する必要がある
  12. 12. #sfdg  トリガ側のサンプル(取引先を利用) trigger AccountTrigger on Account (before insert, before update, ...(その他イベント)) { if( AccountTriggerHandler.hasExecuted ){ return; // prevent recursive re-entry } AccountTriggerHandler.hasExecuted= true; AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size); if(Trigger.isInsert && Trigger.isBefore){ handler.OnBeforeInsert(Trigger.new); } else if(Trigger.isInsert && Trigger.isAfter){ handler.OnAfterInsert(Trigger.new); AccountTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet()); } else if(Trigger.isUpdate && Trigger.isBefore){ handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap); } ... } 各イベントでの処理を、 ハンドラ側で実装する 非同期用の処理も、 ハンドラ側で実装する トリガハンドラを用意する
  13. 13. #sfdg  トリガハンドラのサンプル public with sharing class AccountTriggerHandler { public static boolean hasExecuted = false; private boolean m_isExecuting = false; private integer BatchSize = 0; public AccountTriggerHandler(boolean isExecuting, integer size){ m_isExecuting = isExecuting; BatchSize = size; } public void OnBeforeInsert(Account[] newAccounts){ // 何かの処理 } public void OnAfterInsert(Account[] newAccounts){ // 何かの処理 } @future public static void OnAfterInsertAsync(Set<ID> newAccountIDs){ // 何かの非同期処理 List<Account> newAccounts = [select Id, Name from Account where Id IN :newAccountIDs]; } ... } トリガで発生したイベントからの 処理を実装する
  14. 14. #sfdg  トリガハンドラ側の実装 public with sharing class AccountTriggerHandler { public static boolean hasExecuted = false; private boolean m_isExecuting = false; private integer BatchSize = 0; public AccountTriggerHandler(boolean isExecuting, integer size){ m_isExecuting = isExecuting; BatchSize = size; } ... } 再帰処理防止のためのstatic変数
  15. 15. #sfdg  トリガ側の実装 trigger AccountTrigger on Account (before insert, before update, ...(その他イベント)) { if( AccountTriggerHandler.hasExecuted ){ return; // prevent recursive re-entry } AccountTriggerHandler.hasExecuted= true; AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size); if(Trigger.isInsert && Trigger.isBefore){ handler.OnBeforeInsert(Trigger.new); } else if(Trigger.isInsert && Trigger.isAfter){ handler.OnAfterInsert(Trigger.new); AccountTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet()); } else if(Trigger.isUpdate && Trigger.isBefore){ handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap); } ... } ①トリガ実行時にstatic変数のフラグをtrueにする ②再帰時にはトリガをスキップさせる
  16. 16. #sfdg  トリガハンドラのサンプル public with sharing class AccountTriggerHandler { ... public void OnBeforeInsert(Account[] newAccounts) { // 何かの処理 } @future public static void OnAfterInsertAsync(Set<ID> newAccountIDs){ // 何かの非同期処理 List<Account> newAccounts = [select Id, Name from Account where Id IN :newAccountIDs]; } public void OnBeforeUpdate(Account[] oldAccounts, Account[] updatedAccounts, Map<ID, Account> accountMap){ //Example Map usage Map<ID, Contact> contacts = new Map<ID, Contact>([select Id, FirstName, LastName, Email from Contact where AccountId IN :accountMap.keySet()]); } } • トリガハンドラにて各イベントに 応じた処理を実装する • トリガ内で利用できる変数を考慮して、 引数を決めておく
  17. 17. #sfdg  トリガ側の実装 trigger AccountTrigger on Account (before insert, before update, ...(その他イベント)) { ... AccountTriggerHandler.hasExecuted= true; AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size); if(Trigger.isInsert && Trigger.isBefore){ handler.OnBeforeInsert(Trigger.new); } else if(Trigger.isInsert && Trigger.isAfter){ handler.OnAfterInsert(Trigger.new); AccountTriggerHandler.OnAfterInsertAsync(Trigger.newMap.keySet()); } else if(Trigger.isUpdate && Trigger.isBefore){ handler.OnBeforeUpdate(Trigger.old, Trigger.new, Trigger.newMap); } ... } 各イベントに対応するトリガハンドラ側の メソッドを呼び出す
  18. 18. #sfdg  テンプレート導入によるメリット  同じところはコピー&ペーストで済ませられる  ロジックの実装によりフォーカスできる
  19. 19. #sfdg
  20. 20. #sfdg  前述のトリガテンプレート含め、「トリガハンドラ用のApexクラスを実装して 処理を移譲する」が推奨パターンとして広く周知されてきた  しかし、トリガでの実装がどんどん増えてくると、トリガハンドラも巨大な Apexクラスとなり、メンテナンスが困難に...?  1つのApexクラスで対応するのが困難であれば、Apexクラスを分割してはどう か?  トリガでの実装で、機能単位でApexクラスを分割して、各Apexクラスで 1つのことをうまくやるように考えてみる ※次ページ以降のソースコードは https://github.com/takahiro-yonei/ApexTriggerForMeetup18 で公開してます
  21. 21. #sfdg  トリガのサンプル trigger AccountTrigger on Account (before insert, before update, after insert) { AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size); handler.addObserver(new TriggerOperation[]{TriggerOperation.BEFORE_INSERT, TriggerOperation.BEFORE_UPDATE}, new AccountTriggerHandler_Validation()); handler.addObserver(new TriggerOperation[]{TriggerOperation.AFTER_INSERT}, new AccountTriggerHandler_FeedToXXX()); switch on Trigger.operationType { when BEFORE_INSERT { handler.onBeforeInsert(Trigger.new); } when BEFORE_UPDATE { handler.onBeforeUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap); } when AFTER_INSERT { handler.onAfterInsert(Trigger.new); } } } • トリガで処理したい内容を、適度な大きさで 分割する • 分割したApexクラスを、処理したい順番でト リガハンドラに保持させる
  22. 22. #sfdg  分割したApexクラス側の実装 (1) public without sharing class AccountTriggerHandler_Validation implements BaseTriggerHandler.ObserverTrgBeforeInsert, BaseTriggerHandler.ObserverTrgBeforeUpdate { public AccountTriggerHandler_Validation(){} public List<SObject> onBeforeInsert(List<SObject> newAccounts){ for(Account acc : (List<Account>)newAccounts){ // Some Validation Process...etc System.debug('onBeforeInsert: ' + acc); } return newAccounts; } public List<SObject> onBeforeUpdate(List<SObject> oldAccounts, List<SObject> updAccounts, Map<Id, SObject> oldAccountMap,Map<Id, SObject> updAccountMap){ for(Account acc : (List<Account>)updAccounts){ // Some Validation Process...etc System.debug('onBeforeUpdate: ' + acc); } return updAccounts; } } • Before Insert, Before Updateの 時だけ反応する
  23. 23. #sfdg  分割したApexクラス側の実装 (2) public without sharing class AccountTriggerHandler_FeedToXXX implements BaseTriggerHandler.ObserverTrgAfterInsert { public AccountTriggerHandler_FeedToXXX() {} public void onAfterInsert(List<SObject> newAccounts){ for(Account acc : (List<Account>)newAccounts){ // Some Process System.debug('onAfterInsert: ' + acc); } } } • After Insertの時だけ反応する
  24. 24. #sfdg  中の仕組みについて (0)  登場するApexクラス BaseTriggerHandler Account TriggerHandler Account TriggerHandler_XXX • 実際にトリガ処理を実装するところ 呼び出す インタフェースを 実装 継承 Account Trigger
  25. 25. #sfdg  中の仕組みについて (1) public without sharing class AccountTriggerHandler extends BaseTriggerHandler { private static Boolean InProcess = false; public AccountTriggerHandler(boolean pIsExecuting, Integer pSize){ super(pIsExecuting, pSize); } public void onBeforeInsert(Account[] newAccounts){ for(BaseTriggerHandler.ObserverTrgBeforeInsert observer : beforeInsertObservers){ newAccounts = (List<Account>)observer.onBeforeInsert(newAccounts); } } public void onAfterInsert(Account[] newAccounts){ for(BaseTriggerHandler.ObserverTrgAfterInsert observer : afterInsertObservers){ observer.onAfterInsert(newAccounts); } } ... • 各イベントに応じたインタフェースを実装した Apexクラスのメソッドを順次実行していく
  26. 26. #sfdg  中の仕組みについて (2) public virtual class BaseTriggerHandler { public virtual interface ObserverTrg {} public interface ObserverTrgBeforeInsert extends ObserverTrg { List<SObject> onBeforeInsert(List<SObject> newObjects); } public interface ObserverTrgBeforeUpdate extends ObserverTrg { List<SObject> onBeforeUpdate(List<SObject> oldObjects, List<SObject> newObjects, Map<Id, SObject> oldObjectMap, Map<Id, SObject> newObjectMap); } public interface ObserverTrgBeforeDelete extends ObserverTrg { List<SObject> onBeforeDelete(List<SObject> delObjects); } public interface ObserverTrgAfterInsert extends ObserverTrg { void onAfterInsert(List<SObject> newObjects); } public interface ObserverTrgAfterUpdate extends ObserverTrg { void onAfterUpdate(List<SObject> oldObjects, List<SObject> newObjects, Map<Id, SObject> oldObjectMap, Map<Id, SObject> newObjectMap); } public interface ObserverTrgAfterDelete extends ObserverTrg { void onAfterDelete(List<SObject> delObjects); } • トリガイベントに応じたインタフェースを 定義しておく • 各種イベントで処理したい内容を持つApex クラスを用意し、そのイベント用のインタ フェースを実装する
  27. 27. #sfdg  中の仕組みについて (3) public virtual class BaseTriggerHandler { ... protected Boolean IsExecuting = false; protected Integer BatchSize = 0; protected List<ObserverTrgBeforeInsert> beforeInsertObservers; protected List<ObserverTrgBeforeUpdate> beforeUpdateObservers; protected List<ObserverTrgBeforeDelete> beforeDeleteObservers; protected List<ObserverTrgAfterInsert> afterInsertObservers; protected List<ObserverTrgAfterUpdate> afterUpdateObservers; protected List<ObserverTrgAfterDelete> afterDeleteObservers; public BaseTriggerHandler(boolean param_IsExecuting, Integer param_Size){ this.IsExecuting = param_IsExecuting; this.BatchSize = param_Size; beforeInsertObservers = new List<ObserverTrgBeforeInsert>(); beforeUpdateObservers = new List<ObserverTrgBeforeUpdate>(); beforeDeleteObservers = new List<ObserverTrgBeforeDelete>(); afterInsertObservers = new List<ObserverTrgAfterInsert>(); afterUpdateObservers = new List<ObserverTrgAfterUpdate>(); afterDeleteObservers = new List<ObserverTrgAfterDelete>(); } • 各イベントで処理させるApexクラスを格 納するためのリスト • 各イベントでは、このリストにあるApex クラスを順次処理していく
  28. 28. #sfdg  中の仕組みについて (4) public virtual class BaseTriggerHandler { ... public void addObserver(List<System.TriggerOperation> points, ObserverTrg ob){ for(System.TriggerOperation o : points){ switch on o { when BEFORE_INSERT { beforeInsertObservers.add((ObserverTrgBeforeInsert)ob); } when BEFORE_UPDATE { beforeUpdateObservers.add((ObserverTrgBeforeUpdate)ob); } when BEFORE_DELETE { beforeDeleteObservers.add((ObserverTrgBeforeDelete)ob); } when AFTER_INSERT { afterInsertObservers.add((ObserverTrgAfterInsert)ob); } when AFTER_UPDATE { afterUpdateObservers.add((ObserverTrgAfterUpdate)ob); } ...(略) when else {} } } } • 各イベントに応じたインタフェースを実 装したApexクラスをリストに追加する • トリガ起動時に、Apexクラスのインスタ ンスを、リストに追加していく
  29. 29. #sfdg trigger AccountTrigger on Account (before insert, before update, after insert) { AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.isExecuting, Trigger.size); handler.addObserver(new TriggerOperation[]{TriggerOperation.BEFORE_INSERT, TriggerOperation.BEFORE_UPDATE}, new AccountTriggerHandler_Validation()); handler.addObserver(new TriggerOperation[]{TriggerOperation.AFTER_INSERT}, new AccountTriggerHandler_FeedToXXX()); switch on Trigger.operationType { when BEFORE_INSERT { handler.onBeforeInsert(Trigger.new); } when BEFORE_UPDATE { handler.onBeforeUpdate(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap); } when AFTER_INSERT { handler.onAfterInsert(Trigger.new); } } } • 分割したApexクラスを、処理したい順番でト リガハンドラに保持させる  中の仕組みについて (5)
  30. 30. #sfdg  メリット vs デメリット  メリット  各Apexクラスの責務が明確になって、メンテナンス性は維持できる  各Apexクラスの処理順序も制御できる  デメリット  分割するApexクラスの粒度 • あんまり細かく分けてもApexクラスが大量に作成されて、メンテナン ス性が落ちるかもしれない • チーム内で、分割する基準を共有しておく必要がある  トリガ処理の効率 • 分割したApexクラスごとにfor-loopが実行されるので、トリガハンドラ が単体の場合よりも処理効率は落ちる • (まだあまり大きな問題になったことはないけど...)
  31. 31. #sfdg  Apexトリガは非常に有用だが、毎回ゼロから実装するのは手間がかかるし、ト リガ本体に全て詰め込むとメンテナス困難になる、という経緯からテンプレー トが考案され、広まった(と思う)  テンプレートを用意することで、重要なロジックの実装により集中できる  トリガハンドラに移譲する仕組みは良いが、継続的な改善でトリガハンドラ が巨大になってメンテナンス困難になる可能性もある  トリガハンドラに移譲する仕組みを発展させて、トリガハンドラを分割するこ とを検討してみた  巨大になりがちなトリガハンドラの可読性とメンテナンス性を維持する上で は有効(と思う)  今後の発展として、プロセスビルダーとApexトリガはどこまで共存できるか? について検討してみたい(※気が向いたら)
  32. 32. #sfdg
  • issei_matsumoto

    Feb. 19, 2020
  • koichirokawaguchi

    Feb. 16, 2020
  • smilesuga

    Nov. 20, 2019
  • tak4hir0

    Jun. 29, 2018
  • rihitosaito

    Jun. 28, 2018
  • ssuserdaa3c1

    Jun. 28, 2018

Apex Trigger (Tokyo Salesforce Developer Group Meetup #18)

Views

Total views

4,907

On Slideshare

0

From embeds

0

Number of embeds

250

Actions

Downloads

19

Shares

0

Comments

0

Likes

6

×