【Salesforce】レコードの変更通知を（ほぼ）フローのみで実装する

# はじめに

以下のようなChatter通知をレコードトリガフローで行う方法をご紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/d0873c09-7527-5428-017e-71cdcf6f62b2.png)

しかし、フローのみで変更された項目を抽出するのはかなり面倒でメンテナンスも大変です。
そこで今回は一部Apexを使用し、どのオブジェクトに対しても使用可能かつ項目が増えても修正不要な汎用クラスを実装します。
フローからApexを呼び出す基本的な方法についても解説するので、苦手意識がある方もぜひ挑戦してみてください！

[Apex を使用したフローの拡張](https://help.salesforce.com/s/articleView?id=sf.flow_build_extend_apex_container.htm&type=5)

- [1. Apex実装](#1-apex実装)
- [2. フロー実装](#2-フロー実装)
- [3. おわりに](#3-おわりに)

# 1. Apex実装

## 【1】Apexを使う理由
リリースの度にアップデートされどんどん使いやすくなっているフローですが、ちょっと複雑な処理になるとついついApexで書いたほうが早いな、、となりがちです。
Apexを使用せずフローのみで今回のような通知機能を実装しようとすると、項目の数だけ数式や割り当て要素を使って判定する地道な方法を取らざるを得ないです。
しかも項目が増える度にフローの修正が必要になるのでメンテナンスしきれない事態に、、

しかしApexを使用すればオブジェクトが持つ全項目が取得できるので、項目が増えてもフローの修正は必要ありません。
今回は「値が変更されたかどうか」「変更された項目名の文字列」「変更前後の値を格納するリスト」の3つを情報を取得できるクラスを紹介します。

## 【2】Apex実装
```Apex:FieldModification.cls
// 変更された項目毎の情報を管理するクラス
global without sharing class FieldModification {
    @AuraEnabled
    global String fieldLabel;
    @AuraEnabled
    global String oldValue;
    @AuraEnabled
    global String newValue;

    public FieldModification(String fieldLabel, Object oldValue, Object newValue) {
        this.fieldLabel = fieldLabel;
        this.oldValue = oldValue != null ? String.valueOf(oldValue) : '\"\"';
        this.newValue = newValue != null ? String.valueOf(newValue) : '\"\"';
    }
}
```
```Apex:FieldModificationObserver.cls
// フローから呼び出し、変更された項目情報を抽出するクラス
global without sharing class FieldModificationObserver {
    // フローから受け取る情報を格納するクラス
    global class Input {
        @InvocableVariable(required=true label='変更前のレコード')
        global SObject oldRecord;
        @InvocableVariable(required=true label='変更後のレコード')
        global SObject newRecord;
        @InvocableVariable(required=true label='区切り文字')
        global String separator;
    }

    // フローへ返却する情報を格納するクラス
    global class Output {
        @InvocableVariable
        global List<FieldModification> fieldDetails;
        @InvocableVariable
        global String changedFieldLabel;
        @InvocableVariable
        global Boolean hasFieldChanged;

        Output(List<FieldModification> fieldDetails, String changedFieldLabel) {
            this.fieldDetails = fieldDetails;
            this.changedFieldLabel = changedFieldLabel;
            this.hasFieldChanged = !fieldDetails.isEmpty();
        }
    }

    @InvocableMethod(label='変更された項目情報を取得')
    global static List<Output> verifyFieldModification(List<Input> inputList){
        Output outputResult = getChangedFields(inputList[0]);
        return new List<Output>{outputResult};
    }

    /**
     * @description 変更された項目情報を特定する
     * @param  input フローから渡される変更前後のレコードと区切り文字
     * @return       変更された項目の情報
     */
    private static Output getChangedFields(Input input){
        SObject oldRecord = input.oldRecord; // 変更前のレコード
        SObject newRecord = input.newRecord; // 変更後のレコード
        String separator = input.separator; // 文字列として返却する項目名の区切り文字

        List<FieldModification> fieldDetails = new List<FieldModification>();
        List<String> changedFieldLabels = new List<String>();

        // 対象オブジェクトの項目情報を取得してループ
        for(SObjectField field : newRecord.getSObjectType().getDescribe().fields.getMap().values()){
            DescribeFieldResult result = field.getDescribe();
            // ユーザが更新可能な変更された項目の場合
            if(oldRecord.get(field) != newRecord.get(field) && result.isUpdateable()){
                // 変更された項目ラベル名・変更前の値・変更後の値を保持するクラスを生成してリストへ格納
                FieldModification fieldDetail = new FieldModification(result.getLabel(), oldRecord.get(field), newRecord.get(field));
                fieldDetails.add(fieldDetail);
                // 項目ラベル名のみを保持するリストへ格納
                changedFieldLabels.add(result.getLabel());
            }
        }

        // 項目ラベル名リストを区切り文字で連結
        String changedFieldLabel = String.join(changedFieldLabels, separator);
        // フローへの返却用クラスとして返す
        return new Output(fieldDetails, changedFieldLabel);
    }
}
```

## 【3】Apex解説
### 【3-1】 フローからの入力
```Apex:FieldModificationObserver.cls
    // フローから受け取る情報を格納するクラス
    global class Input {
        @InvocableVariable(required=true label='変更前のレコード')
        global SObject oldRecord;
        @InvocableVariable(required=true label='変更後のレコード')
        global SObject newRecord;
        @InvocableVariable(required=true label='区切り文字')
        global String separator;
    }
```

まずはフローからの入力に使用するクラスを定義しています。
`@InvocableVariable`を変数に付与することでフローからApexに値を渡すことができます。
今回は全オブジェクトに対応させるため、型を特定のオブジェクトではなく`SObject`に設定しています。

### 【3-2】フローからの呼び出し
```Apex:FieldModificationObserver.cls
    @InvocableMethod(label='変更された項目情報を取得')
    global static List<Output> verifyFieldModification(List<Input> inputList){
        Output outputResult = getChangedFields(inputList[0]);
        return new List<Output>{outputResult};
    }
```

`@InvocableMethod`を付与することでフローの「Apexアクション」で選択可能になります。
引数は【3-1】で定義したクラスの**List型**で指定する点に注意してください。
List型といっても、結果的には最初の要素に全ての入力値が格納されているため、今回のメイン処理部分である`getChangedFields()`に最初の要素のみを渡します。
最後にのちほど説明するフローへの返却用クラスを同じくList型にして返却しています。

### 【3-3】メイン処理
```Apex:FieldModificationObserver.cls
    /**
     * @description 変更された項目情報を特定する
     * @param  input フローから渡される変更前後のレコードと区切り文字
     * @return       変更された項目の情報
     */
    private static Output getChangedFields(Input input){
        SObject oldRecord = input.oldRecord; // 変更前のレコード
        SObject newRecord = input.newRecord; // 変更後のレコード
        String separator = input.separator; // 文字列として返却する項目名の区切り文字

        List<FieldModification> fieldDetails = new List<FieldModification>();
        List<String> changedFieldLabels = new List<String>();

        // 対象オブジェクトの項目情報を取得してループ
        for(SObjectField field : newRecord.getSObjectType().getDescribe().fields.getMap().values()){
            DescribeFieldResult result = field.getDescribe();
            // ユーザが更新可能な変更された項目の場合
            if(oldRecord.get(field) != newRecord.get(field) && result.isUpdateable()){
                // 変更された項目ラベル名・変更前の値・変更後の値を保持するクラスを生成してリストへ格納
                FieldModification fieldDetail = new FieldModification(result.getLabel(), oldRecord.get(field), newRecord.get(field));
                fieldDetails.add(fieldDetail);
                // 項目ラベル名のみを保持するリストへ格納
                changedFieldLabels.add(result.getLabel());
            }
        }

        // 項目ラベル名リストを区切り文字で連結
        String changedFieldLabel = String.join(changedFieldLabels, separator);
        // フローへの返却用クラスとして返す
        return new Output(fieldDetails, changedFieldLabel);
    }
```

:::note warn
Winter22以前は`Record__Prior`をSObject型に代入しようとすると`Unable to convert value...`とエラーが発生していましたが、Spring22から使用可能になりました！
[参考：[Trial] Record__Prior variable passed on to Apex Invocable is throwing 'Cannot convert to Apex type' error](https://trailblazer.salesforce.com/issues_view?id=a1p4V00000041MeQAI&title=record__prior-variable-passed-on-to-apex-invocable-is-throwing-cannot-convert-to-apex-type-error)
:::

ここでは`newRecord.getSObjectType().getDescribe().fields.getMap().values()`でオブジェクトの全項目を取得し、`field.getDescribe()`で項目の詳細情報を取得しています。
項目情報をループさせレコードの値を取得することで、自動的に全項目の値が変更されたか判定できる流れです。
この項目情報には「所有者」「最終更新日」などが含まれてしまうため、現在のユーザが編集できるかどうかを`isUpdateable()`を用いて判定しています。

`FieldModification`は変更された項目の情報格納クラス（【3-4】参照）
また、変更された項目名をフロー側でそのまま表示できるよう`separator`を用いて項目名を連結させておきます。
出力用クラスを生成しつつ返却してメイン部分の処理は完了です！

:::note warn
`isUpdateable()`を用いる弊害として、他のフローやトリガで「実行ユーザに編集権限が与えられていない項目」が変更された場合は、今回のフローによる通知対象に含まれません。
通知を行うためには`isUpdateable()`を削除し、必要に応じて「最終更新日」「System Modstamp」など通知不要な項目を除外してください。
:::

### 【3-4】フローへの出力
```Apex:FieldModificationObserver.cls
    // フローへ返却する情報を格納するクラス
    global class Output {
        @InvocableVariable
        global List<FieldModification> fieldDetails;
        @InvocableVariable
        global String changedFieldLabel;
        @InvocableVariable
        global Boolean hasFieldChanged;

        Output(List<FieldModification> fieldDetails, String changedFieldLabel) {
            this.fieldDetails = fieldDetails;
            this.changedFieldLabel = changedFieldLabel;
            this.hasFieldChanged = !fieldDetails.isEmpty();
        }
    }
```

```Apex:FieldModificationObserver.cls
global without sharing class FieldModification {
    @AuraEnabled
    global String fieldLabel;
    @AuraEnabled
    global String oldValue;
    @AuraEnabled
    global String newValue;

    public FieldModification(String fieldLabel, Object oldValue, Object newValue) {
        this.fieldLabel = fieldLabel;
        this.oldValue = oldValue != null ? String.valueOf(oldValue) : '\"\"';
        this.newValue = newValue != null ? String.valueOf(newValue) : '\"\"';
    }
}
```

`FieldModification`がフローへ返却する`Output`クラスにList型で格納されている形です。
`@AuraEnabled`を付与することでフロー内で変数の型として使用可能になり、のちほどフローで行うループ処理に役立ちます。
`oldValue`と`newValue`は文字列・数値・真偽値など様々な型が想定されるためObject型で受け取り、Stringに変換しておきます。

# 2. フロー実装

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/a775fb9b-ff22-5b4d-8db0-8999d3e7f860.png)

## 【1】レコードトリガフロー作成
今回は取引先レコードが更新される度にフローを起動したいので下記の設定で作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/80b70fb6-b932-6625-c061-ec0dd23a8842.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/4c02dcd3-3725-78fb-2fa2-3584bb90b4ae.png)

## 【2】変数の作成
Apexの実行結果を受け取るため事前に3つの変数を作成しておきます。

・レコードが更新されたか判定する真偽値
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/2a1869a7-ce98-c135-343b-3fd96af10318.png)

・変更された項目名の文字列（「取引先名 / 業種 / 郵便番号」の部分）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/3a09e17a-9dcb-2f4c-c773-3ffb58270356.png)

・変更された項目の詳細（`FieldModification`クラスの情報）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/0a02fed4-0291-a0b4-02d1-0672e407c8eb.png)

## 【3】Apexアクションの設定
【2】で作成した変数を使用してApexアクションを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/f8504c94-19ab-493b-5729-289e669feafc.png)

`Record`が更新後、`Record__Prior`が更新前のレコードです。
区切り文字はお好みで設定してください。

## 【4】決定要素の作成
レコードが変更されていない時（ただ保存された時）は通知が不要なので、決定要素を挟みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/a1d755e9-a771-fd78-9cbe-06384b61f665.png)

## 【5】ループ
`FieldModification`のリストをループして以下の部分を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/74cca80c-6446-4766-65a0-69d33e0f8729.png)

・ループ対象は`FieldModification`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/ae38b369-bd82-28e5-fa97-bd3d3b18bb6a.png)

・一時的に文字列を保管する変数を作成して割り当て
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/5eef1a73-da9d-6fce-d0f6-6e467f598d05.png)

・1つ前で成型した文字列を格納する変数を作成して割り当て、次のループに備えるため一時保管用変数を初期化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/2d5d7f16-e041-1df7-0671-7370de12f45b.png)

以上で「どの項目がどのように変更されたか」を出力するための文字列が作成できました。

## 【6】Chatter投稿

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/4c1e91b5-0745-a854-c65d-3ca93de70471.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/a342772b-3e7b-1be2-0507-524bae2d55c4.png)

# 3. おわりに

実装手順は以上です、お疲れ様でした！
最初は取っ付きにくいフローからのApex呼び出しですが意外と簡単、、！と感じてもらえると嬉しいです。
今回のようにフローから呼び出せる汎用的なApexクラスを用意すれば開発効率アップ間違い無しなので、ぜひぜひ挑戦してみてください！
最後までご覧いただきありがとうございました！