[Salesforce]Apex トリガ勉強メモ

#1.Apex トリガの記述
Apex トリガを使用すると、Salesforce のレコードに対するイベント (挿入、更新、削除) の前または後にカスタムアクションを実行できます。データベースシステムでトリガがサポートされるのと同様に、Apex でもレコードを管理する目的でトリガがサポートされます。

一般に、トリガを使用するのは、特定の条件に基づいて操作を実行する場合、関連レコードを変更する場合、または特定の操作の実行を制限する場合などです。SOQL および DML の実行や、カスタム Apex メソッドのコールなど、Apex で行えることはすべてトリガで実行できます。

#2.一般的な実装例
```java
trigger TriggerName on ObjectName (trigger_events) {
    if (Trigger.isInsert) {
        if (Trigger.isBefore) {
            // Before Insert 処理
        } else if (Trigger.isAfter) {
            // After Insert 処理
    }
    ．．．（略）
}
```
挿入、更新、削除、および復元操作の前または後にトリガを実行する場合、カンマ区切りのリストで複数のトリガイベントを指定します。`trigger_events`指定できるのは次のイベントです。

- before insert
- before update
- before delete
- after insert
- after update
- after delete
- after undelete

**トリガコンテキスト変数**
次の表は、トリガに使用可能なすべてのコンテキスト変数の包括的なリストです。

|     変数     |     使用方法     | 
|:----------------------|:---------------------------------------------|
| isExecuting | Apex コードの現在のコンテキストが Visualforce ページ、Web サービス、または executeanonymous() API コールではなく、トリガである場合、true を返します。 | 
| isInsert | 挿入操作により、Salesforce ユーザインターフェース、Apex、または API からこのトリガが実行された場合に、true を返します。 | 
| isUpdate | 更新操作により、Salesforce ユーザインターフェース、Apex、または API からこのトリガが実行された場合に、true を返します。 | 
| isDelete | 削除操作により、Salesforce ユーザインターフェース、Apex、または API からこのトリガが実行された場合に、true を返します。 | 
| isBefore | レコードが保存される前にこのトリガが実行された場合に、true を返します。 | 
| isAfter | すべてのレコードが保存された後にこのトリガが実行された場合に、true を返します。 | 
| isUndelete | レコードがごみ箱から復元された後にこのトリガが実行された場合に、true を返します。この復元は、Salesforce ユーザインターフェース、Apex、または API からの復元操作の後にのみ行われます。 | 
| new | 新しいバージョンの sObject レコードのリストを返します。 この sObject リストは insert トリガ、update トリガ、および undelete トリガでのみ使用でき、レコードは before トリガでのみ変更できます。 | 
| newMap | 新しいバージョンの sObject レコードへの ID の対応付けです。この対応付けは before update トリガ、after insert トリガ、after update トリガ、および after undelete トリガでのみ使用できます。 | 
| old | 古いバージョンの sObject レコードのリストを返します。 この sObject リストは update トリガと delete トリガでのみ使用できます。 | 
| oldMap | 古いバージョンの sObject レコードへの ID の対応付けです。この対応付けは update トリガと delete トリガでのみ使用できます。 | 
| operationType | 現在の操作に対応する System.TriggerOperation 種別の列挙値を返します。System.TriggerOperation 列挙値の有効な値は、BEFORE_INSERT、BEFORE_UPDATE、BEFORE_DELETE、AFTER_INSERT、AFTER_UPDATE、AFTER_DELETE、AFTER_UNDELETE です。トリガの種類に基づいて、異なるプログラミングロジックを使用する場合は、switch ステートメントを使用して、一意のトリガ実行列挙状態の異なる順列を指定することを検討します。 | 
| size | 古いバージョンと新しいバージョンの両方を含む、トリガ呼び出しのレコードの合計数。 |

#3.トリガと実行の順序

- 01.古いレコードをデータベースからロード（または、upsert 用レコード初期化）
- 02.新しいレコードの値で古い値を上書き
- 03.システムの入力規則
- 04.すべての before トリガ
- 05.システムの入力規則 ＋ カスタム入力規則
- 06.重複ルール
- 07.レコード保存（コミットされていない）
- 08.レコード再ロード
- 09.すべての after トリガ
- 10.割り当てルール
- 11.自動応答ルール
- 12.ワークフロールール
- 13.ワークフロー項目自動更新が存在する場合、レコードが再度更新されます。
- 14.ワークフロー項目自動更新でレコードが更新された場合、標準の入力規則に加えて、before update トリガおよび after update トリガがもう一度 (さらに 1 回のみ) 実行されます。カスタム入力規則、重複ルール、およびエスカレーションルールは再実行されません。
- 15.プロセス
- 16.エスカレーションルール
- 17.積み上げ集計数式の値の更新
- 18.条件に基づく共有の評価
- 19.データベースのコミット
- 20.コミット後のロジック（メールの送信）

#4.DML merge のトリガー実行順
###4.1.削除対象レコード
 - ⅰ.before delete トリガ実行
 - ⅱ.内部処理実行
    - 無効となるレコード削除
    - 新しい親レコードが子レコードに割り当て
    - 削除されたレコードの MasterRecordId 項目設定

 - ⅲ.after delete トリガ実行

###4.2.更新対象レコード
before / after update トリガ実行。

#5.問題分析
- トリガはシステム権限で実行するため、レコード共有権限は無視される
- メソッド単位でテストできないため、テストクラスの作成には不便利
- ロジックは共有できない

#6.Trigger Framework構成図（Lightweight Framework）
![sf_trigger_framework.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538011/445b5df1-f297-fc0d-46bc-bf4514d26617.jpeg)
#7.TriggerHandler基底クラス
```java
public abstract class TriggerHandler {
    public void execute() {
        switch on Trigger.operationType {
            when BEFORE_INSERT  { this.beforeInsert(Trigger.new); }
            when BEFORE_UPDATE  { this.beforeUpdate(Trigger.oldMap, Trigger.newMap); }
            when BEFORE_DELETE  { this.beforeDelete(Trigger.oldMap); }
            when AFTER_INSERT   { this.afterInsert(Trigger.newMap); }
            when AFTER_UPDATE   { this.afterUpdate(Trigger.oldMap, Trigger.newMap); }
            when AFTER_DELETE   { this.afterDelete(Trigger.oldMap); }
            when AFTER_UNDELETE { this.afterUndelete(Trigger.newMap); }
        }
    }

    protected virtual void beforeInsert(List<Sobject> newList) {}
    protected virtual void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void beforeDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterInsert(Map<Id, Sobject> newMap) {}
    protected virtual void afterUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
    protected virtual void afterDelete(Map<Id, Sobject> oldMap) {}
    protected virtual void afterUndelete(Map<Id, Sobject> newMap) {}

    @TestVisible protected Boolean isFieldValueChanged(SObject oldObject, SObject newObject, SObjectField field) {
        Object oldValue = getFieldValue(oldObject, field);
        Object newValue = getFieldValue(newObject, field);
        return oldValue != newValue;
    }

    @TestVisible protected Object getFieldValue(SObject obj, SObjectField field) {
        Object fieldValue = (obj != null) && (field != null) ? obj.get(field) : null;
        return fieldValue;
    }
}
```
#8.業務トリガハンドラクラス実装例
- レコード共有権限で実行するため、`with sharing`キーワードを利用します。
- オブジェクト名と項目名はオブジェクト定義と一致します。(__c含む、大文字と小文字を区別しない)

```java
public with sharing class EmployeeTriggerHandler extends TriggerHandler {
    public override void beforeInsert(List<Sobject> newList) {
        // 業務処理
        addressSettings((List<Employee__c>)newList);
    }
    public override void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {
        // 業務処理
        List<Employee__c> newList = (List<Employee__c>)newMap.values();
        addressSettings(newList);
    }
    
    // 業務処理実装例
    // 処理概要：社員情報保存するときに、
    //         住所CDより住所マスタから住所情報を取得して、
    //         社員情報データに設定します。
    private void addressSettings(List<Employee__c> newList){
        // 保存する社員情報データから、住所CDを取得します。
        // Setを利用して、同じ住所CDは一回取得だけ
        Set<String> addressCdSet = new Set<String>();
        for(Employee__c element:newList){
            If (!String.IsBlank(element.ADDRESS_CD__c)) {
                addressCdSet.add(element.address_cd__c);
            }
        }
        // 住所CDより住所マスタから住所情報を取得します。
        // [:変数] 検索条件のバインド
        List<Address__c> addressList = [SELECT Name,ADDRESS_1__c,ADDRESS_2__c FROM ADDRESS__c WHERE Name IN :addressCdSet];
        
        Map<String,Address__c> addressMap = new Map<String,Address__c>();
        for(Address__c element:addressList){
            addressMap.put(element.Name, element);
        }
        // 取得したの住所情報を社員情報データに設定します。
        for(Employee__c element:newList){
            If (!String.IsBlank(element.ADDRESS_CD__c)) {
                Address__c address = addressMap.get(element.address_cd__c);
                element.address_1__c = address.address_1__c;
                element.address_2__c = address.address_2__c;
            }
        }
    }
}
```

#9.業務トリガ実装例
- すべてトリガイベントを指定します。

```java
trigger EmployeeTrigger on Employee__c (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
    new EmployeeTriggerHandler().execute();
}
```
#10.Triggerの実行制御について
![sf_trigger_framework1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538011/650d4754-5b4c-6dfb-ec63-1a3d6dde87a3.jpeg)