Memos About Salesforce

Salesforceにハマってたこと!

【最新版SFDCトリガーテンプレート】 salesforce apex recipes sfdc トリガーテンプレート

こんにちは、管理人の@Salesforce.Zです。

今日は、最新版のSalesforce SFDC トリガーテンプレートを共有する

Salesforce DeveloperのGitによる持ってきたものである。

trailheadapps/apex-recipes

読んだら得ること

★ 最新版SFDC トリガーテンプレート

目次

最新版SFDC トリガーテンプレート

TriggerHandler

/**
 * トリガハンドラフレームワーク
 */
public virtual class TriggerHandler {
    private static Map<String, LoopCount> loopCountMap;
    private static Set<String> bypassedHandlers;

    @TestVisible
    private TriggerContext context;

    @TestVisible
    private Boolean isTriggerExecuting;

    static {
        loopCountMap = new Map<String, LoopCount>();
        bypassedHandlers = new Set<String>();
    }

    /**
     * TriggerHandlerカスタム例外クラス
     */
    public class TriggerHandlerException extends Exception {
    }

    /**
     * コンストラク
     */
    public TriggerHandler() {
        this.setTriggerContext();
    }

    /**
     * This is main brokering method that is called by the trigger.
     * It's responsible for determining the proper context, and calling the
     * correct method
     * @example
     * AccountTriggerHandler.run();
     */
    public void run() {
        if (!validateRun()) {
            return;
        }

        addToLoopCount();

        switch on context {
            when BEFORE_INSERT {
                this.beforeInsert();
            }
            when BEFORE_UPDATE {
                this.beforeUpdate();
            }
            when AFTER_INSERT {
                this.afterInsert();
            }
            when AFTER_UPDATE {
                this.afterUpdate();
            }
            when BEFORE_DELETE {
                this.beforeDelete();
            }
            when AFTER_DELETE {
                this.afterDelete();
            }
            when AFTER_UNDELETE {
                this.afterUndelete();
            }
        }
    }

    /**
     * Allows developers to prevent trigger loops, or allow
     * a limited number of them by setting the maximum number of times
     * this trigger is called.
     * @param max   A valid number (generally 1) of times you'd like
     * to allow the trigger to run.
     * @example
     * In the context of a TriggerHandler class,
     * this.setMaxLoopCount(5);
     */
    public void setMaxLoopCount(Integer max) {
        String handlerName = getHandlerName();
        if (!TriggerHandler.loopCountMap.containsKey(handlerName)) {
            TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max));
        } else {
            TriggerHandler.loopCountMap.get(handlerName).setMax(max);
        }
    }

    /**
     * ループ回数の最大値をクリアする
     */
    public void clearMaxLoopCount() {
        this.setMaxLoopCount(-1);
    }

    /**
     * この triggerHandler を実装している他のトリガーを
     * 条件付きでバイパス(無効化)する
     */
    public static void bypass(String handlerName) {
        TriggerHandler.bypassedHandlers.add(handlerName);
    }

    /**
     * バイパスされたトリガーハンドラのリストから
     * 指定されたトリガーハンドラのクラス名を削除します。
     */
    public static void clearBypass(String handlerName) {
        TriggerHandler.bypassedHandlers.remove(handlerName);
    }

    /**
     * 与えられたトリガーハンドラクラスが
     * 現在バイパスされているかどうかをチェックする
     */
    public static Boolean isBypassed(String handlerName) {
        return TriggerHandler.bypassedHandlers.contains(handlerName);
    }

    /**
     * バイパスリストからすべてのクラスを削除
     */
    public static void clearAllBypasses() {
        TriggerHandler.bypassedHandlers.clear();
    }

    /***************************************
     * private instancemethods
     ***************************************/

    /**
     * トリガーコンテキストを強制的に設定する
     */
    @TestVisible
    private void setTriggerContext() {
        this.setTriggerContext(null, false);
    }

    /**
     * 手動でトリガーコンテキストを設定する
     */
    @TestVisible
    private void setTriggerContext(String ctx, Boolean testMode) {
        if (!Trigger.isExecuting && !testMode) {
            this.isTriggerExecuting = false;
            return;
        } else {
            this.isTriggerExecuting = true;
        }

        if (Trigger.isExecuting && !testMode) {
            switch on Trigger.operationType {
                when BEFORE_INSERT {
                    context = TriggerContext.BEFORE_INSERT;
                }
                when BEFORE_UPDATE {
                    context = TriggerContext.BEFORE_UPDATE;
                }
                when BEFORE_DELETE {
                    context = TriggerContext.BEFORE_DELETE;
                }
                when AFTER_INSERT {
                    context = TriggerContext.AFTER_INSERT;
                }
                when AFTER_UPDATE {
                    context = TriggerContext.AFTER_UPDATE;
                }
                when AFTER_DELETE {
                    context = TriggerContext.AFTER_DELETE;
                }
                when AFTER_UNDELETE {
                    context = TriggerContext.AFTER_UNDELETE;
                }
            }
        } else if (ctx != null && testMode) {
            switch on ctx {
                when 'before insert' {
                    context = TriggerContext.BEFORE_INSERT;
                }
                when 'before update' {
                    context = TriggerContext.BEFORE_UPDATE;
                }
                when 'before delete' {
                    context = TriggerContext.BEFORE_DELETE;
                }
                when 'after insert' {
                    context = TriggerContext.AFTER_INSERT;
                }
                when 'after update' {
                    context = TriggerContext.AFTER_UPDATE;
                }
                when 'after delete' {
                    context = TriggerContext.AFTER_DELETE;
                }
                when 'after undelete' {
                    context = TriggerContext.AFTER_UNDELETE;
                }
                when else {
                    throw new TriggerHandler.TriggerHandlerException(
                        'Unexpected trigger context set'
                    );
                }
            }
        }
    }

    /**
     * ループ数をインクリメントする
     */
    @TestVisible
    private void addToLoopCount() {
        String handlerName = getHandlerName();
        if (TriggerHandler.loopCountMap.containsKey(handlerName)) {
            Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName)
                .increment();
            if (exceeded) {
                Integer max = TriggerHandler.loopCountMap.get(handlerName).max;
                throw new TriggerHandlerException(
                    'Maximum loop count of ' +
                    String.valueOf(max) +
                    ' reached in ' +
                    handlerName
                );
            }
        }
    }

    /**
     * このトリガーが継続して実行されることを確認する
     */
    @TestVisible
    private Boolean validateRun() {
        if (!this.isTriggerExecuting || this.context == null) {
            throw new TriggerHandlerException(
                'Trigger handler called outside of Trigger execution'
            );
        }
        if (TriggerHandler.bypassedHandlers.contains(getHandlerName())) {
            return false;
        }
        return true;
    }

    /**
     * ハンドラクラス名を取得
     */
    @TestVisible
    private String getHandlerName() {
        return String.valueOf(this)
            .substring(0, String.valueOf(this).indexOf(':'));
    }

    /***************************************
     * context methods
     ***************************************/

    @TestVisible
    protected virtual void beforeInsert() {
    }

    @TestVisible
    protected virtual void beforeUpdate() {
    }

    @TestVisible
    protected virtual void beforeDelete() {
    }

    @TestVisible
    protected virtual void afterInsert() {
    }

    @TestVisible
    protected virtual void afterUpdate() {
    }

    @TestVisible
    protected virtual void afterDelete() {
    }

    @TestVisible
    protected virtual void afterUndelete() {
    }

    /***************************************
     * inner classes
     ***************************************/

    /**
     * ハンドラごとのループ数を管理するための内部クラス
     */
    @TestVisible
    private class LoopCount {
        private Integer max;
        private Integer count;

        /**
         * 5をデフォルトとするループカウンタ
         */
        public LoopCount() {
            this.max = 5;
            this.count = 0;
        }

        /**
         * パラメーターに基づいてループ数を設定
         */
        public LoopCount(Integer max) {
            this.max = max;
            this.count = 0;
        }

        /**
         * カウンタをインクリメントして this.exceeded() の結果を返す
         */
        public Boolean increment() {
            this.count++;
            return this.exceeded();
        }

        /**
         * ループ数を超えようとしているかどうかを判断
         */
        public Boolean exceeded() {
            if (this.max < 0) {
                return false;
            }
            if (this.count > this.max) {
                return true;
            }
            return false;
        }

        /**
         * 現在のループ数を返す
         */
        public Integer getCount() {
            return this.count;
        }

        /**
         * ループの最大値を返す
         */
        public Integer getMax() {
            return this.max;
        }

        /**
         * ループの最大値を設定
         */
        public void setMax(Integer max) {
            this.max = max;
        }
    }

    /**
     * トリガーコンテキスト
     */
    @TestVisible
    private enum TriggerContext {
        BEFORE_INSERT,
        BEFORE_UPDATE,
        BEFORE_DELETE,
        AFTER_INSERT,
        AFTER_UPDATE,
        AFTER_DELETE,
        AFTER_UNDELETE
    }
}

AccountTriggerHandler

/**
 * 取引先トリガハンドラ
 */
public inherited sharing class AccountTriggerHandler extends TriggerHandler {
    private List<Account> triggerNew;
    private List<Account> triggerOld;
    private Map<Id, Account> triggerMapNew;
    private Map<Id, Account> triggerMapOld;

    /**
     * カスタム例外クラス
     */
    public class AccountTriggerHandlerException extends Exception {
    }

    /**
     * コンストラク
     */
    public AccountTriggerHandler() {
        this.triggerOld = (List<Account>) Trigger.old;
        this.triggerNew = (List<Account>) Trigger.new;
        this.triggerMapNew = (Map<Id, Account>) Trigger.newMap;
        this.triggerMapOld = (Map<Id, Account>) Trigger.oldMap;
    }

    /**
     * Before Insert context method
     **/
    public override void beforeInsert() {
        // for (Account newRec : this.triggerNew) {
        // }
    }

    /**
     * After Insert context method.
     **/
    public override void afterInsert() {
        
    }

    /**
     * Before Update context method.
     **/
    public override void beforeUpdate() {
        // for (Account newRec : this.triggerNew) {
        // }
    }

    /**
     * After Update context method.
     **/
    public override void afterUpdate() {
        // for (Account newRec : this.triggerNew) {
        // }
    }
}

AccountTrigger

trigger AccountTrigger on Account(
    before insert,
    after insert,
    before update,
    after update,
    before delete,
    after delete,
    after undelete
) {
    new AccountTriggerHandler().run();
}

終わりに

その他のサイトでもトリガーテンプレートがあるが、一応Salesforce Developer提供したものであり

積極的に使おうと思う。

実際は、基本的な考え方は変わらない

その他トリガーテンプレートのサイト紹介

2011年 [salesforce]トリガーテンプレート

2018年 初心者 SFDCトリガ肥大化(汗) 柔軟性のトリガ 実装論 

2021年 【Salesforce】テンプレート集!