Dependency Injection Sample for Apex Triggerを使って楽しくトリガを書こう

みなさん、こんにちは。本投稿では「Dependency Injection Sample for Apex Trigger」を利用して、Salesforce Platform上で楽しくApexトリガを書いていく方法をご紹介します。

Dependency Injection Sample for Apex Triggerとは

Apexトリガ を最も柔軟で汎用的に書くことを目指したオリジナルのApexフレームワークのサンプルコードです。複雑なApexトリガのコードを書くのに慣れているひとでも、どこにどのようにロジックを書いていけば将来の機能追加に耐えやすい設計となるのか?を考えることがあるかと思います。その問いに対する私からの1つの解が、こちらになります。
Apexコードを変更せず、カスタムメタデータ型レコードを変更するだけでどのApexコードを有効にするのかを操作できるようになっています。現在はサンプルコードのみを公開していますが、今後はテンプレートからオブジェクトごとに自動生成できるツールを展開予定です。乞うご期待。

インストール方法

GitHubに公開しているコードをSalesforce組織へデプロイすれば、すぐに動かすことができます。デプロイはSalesforce CLIとVS Codeを利用しますので、事前にセットアップしておいてください。
まずGitHubのリポジトリをクローンしてVS Codeで開きます。
(まだGitに習熟してないかたは、[Download ZIP]ボタンでダウンロード)

実行するコマンド

git clone https://github.com/takahitomiyamoto/di-sample-apex-trigger.git
cd di-sample-apex-trigger
code .


ダウンロードできたディレクトリを開くと、Salesforce DXスタイルの構造になっています。scratch組織へプッシュすることはもちろん、最新のVS Code拡張機能により、non-scratch組織、すなわちdeveloper組織やsandbox組織にもデプロイできます。どちらでも好きな方を選んでください。

scratch組織



developer組織

動作確認

デプロイができたら、サンプルコードの動作確認をします。取引先レコードを新規登録または更新時にApexトリガが起動することを確認しましょう。
  1. 新規登録時に、取引先名(Name)項目の先頭に[サンプル]という文字列を付ける。
  2. 新規登録または更新時に、Fax(FAX)項目に値が入っており、かつ電話(Phone)項目に値が入っていない場合は「FAXが入っている場合は電話番号も入力してください」というエラーにする。 

利用法① - トリガ実行タイミングを変更する

上記のロジックが実行されるタイミングを次の変更してみましょう。
  • 更新時に、取引先名(Name)項目の先頭に[サンプル]という文字列を付ける。

(1) Apexクラスを変更

「取引先名(Name)項目の先頭に[サンプル]という文字列を付ける」というロジックは、AccountTriggerService.cls に記載されている addPrefixToName メソッドに対応します。現在は、onBeforeInsert メソッドからのみ呼び出されていますので、onBeforeUpdate メソッドからも呼び出されるように変更します。これ以外、何も変更する必要がありません。Apexトリガや共通のApexトリガハンドラを変更する必要がありません。これがこのフレームワークの特徴です。

変更前

public void onBeforeUpdate() {}

変更後

public void onBeforeUpdate() {
methodName = 'onBeforeUpdate';
if (null == handler) {
return;
}
this.addPrefixToName((List<Account>)handler.newObjects);
}

(2) カスタムメタデータ型のレコードを変更

AccountTriggerService.cls に対応するカスタムメタデータ型のレコードで「BEFORE UPDATE」のみがチェックされるように変更します。
動作確認してみましょう。新規登録時には何もなく、更新時に[サンプル]が追加されましたよね?

また、いずれのタイミングでも実行されたくないロジックは、Apexコードを変更することなく、「Active」のチェックを外して保存すればOKです。これも簡単ですね。

利用法② - 別のオブジェクトのトリガを追加する

サンプルコードにはAccountを利用していますが、別のオブジェクト、たとえばCaseに対してトリガを追加してみましょう。この方法をマスターすれば、様々な場面でこのフレームワークを利用できるようになるでしょう。
では、次のロジックを追加してください。
  • 新規登録時に、件名(Subject)項目の先頭に[テスト]という文字列を付ける。

(1) Apexトリガを追加

  1. CaseTrigger.trigger を作成し、AccountTrigger.trigger の内容をコピー
  2. 「Account」を「Case」に置換
/***********************************
* Name: CaseTrigger.trigger
* Description: trigger for Case
*
* @param before insert
* @param before update
* @param before delete
* @param after insert
* @param after update
* @param after delete
* @param after undelete
* @return none
***********************************/
trigger CaseTrigger on Case(
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete
) {
CommonTriggerHandler handler = new CommonTriggerHandler(Case.class.getName());
handler.invoke();
}

(2) Apexクラスを追加

  1. CaseConstants.cls を作成し、AccountConstants.cls の内容をコピー
  2. 「Account」を「Case」に置換
  3. [テスト]という文字列用の定数 NAME_PREFIX_TEST を追加
  4. 不要なコードを削除
/****************************************
* Name: CaseConstants.cls
* Description: Constants for Case
****************************************/
public without sharing class CaseConstants {
// CaseTriggerService
public static final String NAME_PREFIX_TEST = '[テスト]';
}

  1. CaseTriggerService.cls を作成し、AccountTriggerService.cls の内容をコピー
  2. 「Account」を「Case」に置換
  3. NAME_PREFIX_TEST を追加
  4. 件名(Subject)項目の先頭に[テスト]という文字列を付ける addPrefixToSubject メソッドを追加
  5. onBeforeInsert メソッドから addPrefixToSubject メソッドを呼び出し
/**************************************************
* Name: CaseTriggerService.cls
* Description: Service for Case trigger
**************************************************/
public with sharing class CaseTriggerService implements ITriggerObserver {
// Custom Exception
private class CommonException extends Exception {}
// Constants
private static final System.loggingLevel LOGGING_LEVEL_DEFAULT = CommonLogger.LOGGING_LEVEL_DEFAULT;
private static final String CLASS_NAME = CaseTriggerService.class.getName();
private static final String NAME_PREFIX_TEST = CaseConstants.NAME_PREFIX_TEST;
// Logger Variable
private final CommonLogger logger = CommonLogger.getInstance();
// Class Variables
private static String methodName;
// Instance Variables
private CommonTriggerHandler handler;
/**************************************************
* Name: CaseTriggerService
* Description: Constructor with no parameter
*
* @param none
* @return CaseTriggerService
**************************************************/
public CaseTriggerService() {}
/****************************************
* Name: getTriggerObserver
* Description: Get TriggerObserver
*
* @param CommonTriggerHandler handler
* @return ITriggerObserver
****************************************/
public ITriggerObserver getTriggerObserver(CommonTriggerHandler handler) {
this.handler = handler;
return (ITriggerObserver)this;
}
/*******************************************************
* Name: onBeforeInsert
* Description: Custom action in case of before insert
*
* @param null
* @return void
********************************************************/
public void onBeforeInsert() {
methodName = 'onBeforeInsert';
if (null == handler) {
return;
}
this.addPrefixToSubject((List<Case>)handler.newObjects);
}
/*******************************************************
* Name: onBeforeUpdate
* Description: Custom action in case of before update
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onBeforeUpdate() {}
/*******************************************************
* Name: onBeforeDelete
* Description: Custom action in case of before delete
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onBeforeDelete() {}
/*******************************************************
* Name: onAfterInsert
* Description: Custom action in case of after insert
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onAfterInsert() {}
/*******************************************************
* Name: onAfterUpdate
* Description: Custom action in case of after update
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onAfterUpdate() {}
/*******************************************************
* Name: onAfterDelete
* Description: Custom action in case of after delete
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onAfterDelete() {}
/*******************************************************
* Name: onAfterUndelete
* Description: Custom action in case of after undelete
*
* @param null
* @return void
* @deprecated
********************************************************/
public void onAfterUndelete() {}
/*****************************************************************
* Name: addPrefixToSubject
* Description: 件名(Subject)項目の先頭に[テスト]という文字列を付ける。
*
* @param List<Case> cases
* @return void
*****************************************************************/
@TestVisible
private void addPrefixToSubject(List<Case> cases) {
methodName = 'addPrefixToSubject';
if (null == cases) {
return;
}
for (Case a : cases) {
if (String.isEmpty(a.Subject)) {
a.Subject = '';
}
a.Subject = NAME_PREFIX_TEST + a.Subject;
logger.stackDebugLog(LOGGING_LEVEL_DEFAULT, CLASS_NAME, methodName, 'Subject: ' + a.Subject);
}
}
}

(3) カスタムメタデータ型のレコードを追加

  1. Trigger_Observer.CaseTriggerService.md-meta.xml を作成し、Trigger_Observer.AccountTriggerService.md-meta.xml の内容をコピー
  2. 「Account」を「Case」に置換
  3. 「Before_Insert__c」を「true」、それ以外を「false」
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>CaseTriggerService</label>
<protected>false</protected>
<values>
<field>Active__c</field>
<value xsi:type="xsd:boolean">true</value>
</values>
<values>
<field>After_Delete__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>After_Insert__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>After_Undelete__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>After_Update__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>Apex_Class__c</field>
<value xsi:type="xsd:string">CaseTriggerService</value>
</values>
<values>
<field>Before_Delete__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>Before_Insert__c</field>
<value xsi:type="xsd:boolean">true</value>
</values>
<values>
<field>Before_Update__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
<values>
<field>Object__c</field>
<value xsi:type="xsd:string">Case</value>
</values>
<values>
<field>Test_Only__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
</CustomMetadata>

(4) package.xml に資源を追加

  • ApexClass
    1. CaseConstants
    2. CaseTriggerService
  • ApexTrigger
    1. CaseTrigger
  • CustomMetadata
    1. Trigger_Observer.CaseTriggerService
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<name>ApexClass</name>
<members>AccountConstants</members>
<members>AccountTestUtils</members>
<members>AccountTriggerService</members>
<members>AccountTriggerServiceTest</members>
<members>AccountTriggerValidation</members>
<members>AccountTriggerValidationTest</members>
<members>CaseConstants</members>
<members>CaseTriggerService</members>
<members>CommonConstants</members>
<members>CommonErrorUtils</members>
<members>CommonErrorUtilsTest</members>
<members>CommonLogger</members>
<members>CommonLoggerTest</members>
<members>CommonTestUtils</members>
<members>CommonTriggerHandler</members>
<members>CommonTriggerHandlerTest</members>
<members>ITriggerObserver</members>
</types>
<types>
<name>ApexTrigger</name>
<members>AccountTrigger</members>
<members>CaseTrigger</members>
</types>
<types>
<name>CustomMetadata</name>
<members>Logger.DEFAULT</members>
<members>Trigger_Observer.AccountTriggerService</members>
<members>Trigger_Observer.AccountTriggerValidation</members>
<members>Trigger_Observer.CaseTriggerService</members>
<members>Trigger_Observer.CommonTriggerObserverMock</members>
</types>
<types>
<name>Layout</name>
<members>Logger__mdt-Logger Layout</members>
<members>Trigger_Observer__mdt-Trigger Observer Layout</members>
</types>
<types>
<name>CustomObject</name>
<members>Logger__mdt</members>
<members>Trigger_Observer__mdt</members>
</types>
<types>
<name>CustomField</name>
<members>Logger__mdt.Logging_Level__c</members>
<members>Trigger_Observer__mdt.Active__c</members>
<members>Trigger_Observer__mdt.After_Delete__c</members>
<members>Trigger_Observer__mdt.After_Insert__c</members>
<members>Trigger_Observer__mdt.After_Undelete__c</members>
<members>Trigger_Observer__mdt.After_Update__c</members>
<members>Trigger_Observer__mdt.Apex_Class__c</members>
<members>Trigger_Observer__mdt.Before_Delete__c</members>
<members>Trigger_Observer__mdt.Before_Insert__c</members>
<members>Trigger_Observer__mdt.Before_Update__c</members>
<members>Trigger_Observer__mdt.Object__c</members>
<members>Trigger_Observer__mdt.Test_Only__c</members>
</types>
<types>
<name>ListView</name>
<members>Logger__mdt.All_Records</members>
<members>Trigger_Observer__mdt.All_Records</members>
</types>
<types>
<name>Profile</name>
<members>Admin</members>
</types>
<version>44.0</version>
</Package>
view raw package.xml hosted with ❤ by GitHub

こだわりポイント

  • オブジェクトごとに定数クラス(AccountConstants、CaseConstants)を分けています。CommonConstants にまとめても良いが、まとめることによる肥大化を避けたいためです。
  • Apexトリガ内には具体的なロジックを書きません。オブジェクトに関わらず統一的な順序で処理を実行したいためです。
  • Apexトリガのロジック用のクラス(AccountTriggerService、AccountTriggerValidation、CaseTriggerService)は必ず ITriggerObserver インターフェースを実装するルールとします。誰が書いてもロジックの記述場所を統一させたいためです。
  • AccountTriggerServiceとAccountTriggerValidationは同じクラスにまとめて書いても構いませんが、呼び出したいメソッド(addPrefixToName、ValidatePhone)は1つにまとめずに分けて書いてください。1つのものに複数の意味が含まれていると、カスタムメタデータ型の側から操作しにくくなります。
  • カスタムメタデータ型レコードの名前は、対応するApexクラス名と一致させることでわかりやすくします。
  • テストクラス実行のためだけのカスタムメタデータ型レコード CommonTriggerObserverMock を利用します。テストカバー率を向上させるためです。

さいごに

この書き方をマスターして、いままで以上の生産性を発揮していきましょう。
Happy Coding!!

コメント