SalesforceでFlowとApexが同じレコードを更新するとどうなる?値の上書きと再実行を検証

SalesforceのFlowとApexが同じレコードを更新し再実行される流れを検証する図

Salesforceで同じオブジェクトにレコードトリガーフローとApexトリガーがあると、「どちらの値が最後に残るのか」「同じレコードを更新したら、処理はもう一度動くのか」が分かりにくくなります。

公式ドキュメントには保存処理の実行順序が掲載されていますが、実際の構成では値の上書きや再実行まで含めて考える必要があります。今回は取引先の説明項目をFlowとApexの両方から更新し、最終的な値とデバッグログを確認します。

今回の結論

FlowとApexで同じ[説明]項目を上書きした結果、最終的にはafter-save Flowが設定した「04: after-save Flow」が残りました。

設計上の大前提として、今回のFlowとApexの混在は挙動確認のための構成です。実運用では、同じオブジェクトを起点にするレコードトリガーフローとApexトリガーを別々の入口として併用するのは原則避け、どちらかへ責務を寄せます。

目次

今回確認すること

取引先名を変更したときに、次の4つの処理を動かします。

  1. before-saveレコードトリガーフローで説明を更新する
  2. before update Apexトリガーで説明を上書きする
  3. after update Apexトリガーで、その時点の値をログへ出力する
  4. after-saveレコードトリガーフローで同じ取引先の説明を再更新する

確認したいのは、公式の実行順序そのものではありません。同じ項目を複数の自動化が更新したときの最終値と、after-save Flowによる再更新でApexが何回動くかを主題にします。

確認項目実測結果
before Apexが受け取った値01: before-save Flow
最初のafter Apexが受け取った値02: before update Apex
Apexトリガーの実行回数before/afterともに2回
保存後の説明04: after-save Flow
ボンボン

順番を覚えるだけじゃなく、2回目の更新で何がもう一度動くのかまで見てみよう。

検証環境と前提条件

今回は標準オブジェクトの取引先と、標準項目の取引先名説明を使用します。カスタム項目は作成しません。

検証日2026年7月22日
組織種別Developer Edition
SalesforceリリースSummer ’26
APIバージョン67.0
オブジェクト取引先(Account)
更新の起点取引先名の変更
確認方法取引先の保存結果、デバッグログ

既存のFlow、Apexトリガー、入力規則などがある組織では結果へ影響する可能性があります。検証用組織で試すか、取引先にほかの自動化がないことを確認してから進めます。

検証前に取引先のレコードトリガーフローを確認する画面
検証前に取引先のApexトリガーを確認する画面

1.実行回数を記録するApexクラスを作成する

再更新でトリガーが何回起動したか分かるように、beforeとafterの実行回数を保持するクラスを作成します。クラス名はAccountFlowApexTraceとしました。

public class AccountFlowApexTrace {
    public static Integer beforeCount = 0;
    public static Integer afterCount = 0;
}

static変数は同じトランザクション内で値を保持するため、再更新によって同じトリガーが起動した場合も回数を数えられます。

FlowとApexの実行回数を記録するAccountFlowApexTraceクラス

2.Apexトリガーハンドラーを作成する

まず、before updateとafter updateで実行する処理をまとめたトリガーハンドラーを作成します。ログを絞り込みやすいように、出力するメッセージの先頭はORDER_TESTで統一しました。

public class AccountTriggerHandler {
    public static void beforeUpdate(
        List<Account> newRecords,
        Map<Id, Account> oldMap
    ) {
        // 同一トランザクション内でのbefore updateの実行回数を記録
        AccountFlowApexTrace.beforeCount++;

        for (Account currentRecord : newRecords) {
            // 更新前の取引先を取得
            Account oldRecord = oldMap.get(currentRecord.Id);

            // 実行回数、取引先名の変更有無、現在の説明をログへ出力
            System.debug(
                LoggingLevel.ERROR,
                'ORDER_TEST | before update #'
                + AccountFlowApexTrace.beforeCount
                + ' | Name changed='
                + (currentRecord.Name != oldRecord.Name)
                + ' | Description='
                + currentRecord.Description
            );

            // 取引先名が変更された場合のみ、説明をApex側の値で上書き
            if (currentRecord.Name != oldRecord.Name) {
                currentRecord.Description = '02: before update Apex';
            }
        }
    }

    public static void afterUpdate(List<Account> newRecords) {
        // 同一トランザクション内でのafter updateの実行回数を記録
        AccountFlowApexTrace.afterCount++;

        for (Account currentRecord : newRecords) {
            // 実行回数と、保存後の説明をログへ出力
            System.debug(
                LoggingLevel.ERROR,
                'ORDER_TEST | after update #'
                + AccountFlowApexTrace.afterCount
                + ' | Description='
                + currentRecord.Description
            );
        }
    }
}

beforeUpdateメソッドでは、最初に説明の値をログへ出力します。取引先名が変更された更新だけ、02: before update Apexへ上書きします。

2回目の更新はafter-save Flowによる説明だけの変更なので、取引先名は変わりません。実際のログでもハンドラーは呼び出されましたが、beforeUpdateメソッドによる値の上書きは行われませんでした。

取引先更新時の処理をまとめたApexトリガーハンドラー

3.before updateとafter updateのApexトリガーを作成する

続いて、取引先の更新時に動くApexトリガーを作成します。トリガー側には実処理を書かず、実行タイミングに応じて先ほどのトリガーハンドラーを呼び出すだけにします。

trigger AccountTrigger on Account (
    before update,
    after update
) {
    if (Trigger.isBefore) {
        AccountTriggerHandler.beforeUpdate(
            Trigger.new,
            Trigger.oldMap
        );
    } else if (Trigger.isAfter) {
        AccountTriggerHandler.afterUpdate(
            Trigger.new
        );
    }
}

この構成なら、トリガーは処理の入口、トリガーハンドラーは具体的な処理という役割が分かれます。処理が増えた場合も、トリガー本体を大きくせずハンドラー側で管理できます。

before updateとafter updateでトリガーハンドラーを呼び出すApexトリガー

4.before-saveレコードトリガーフローを作成する

1つ目のFlowは、取引先を更新したときに動くbefore-saveフローです。開始条件は「取引先名が変更された」、最適化の対象は「高速項目更新」にします。

オブジェクト取引先
トリガーレコードが更新された
エントリ条件取引先名/変更済み/True
最適化高速項目更新
取引先名の変更時に動くbefore-saveレコードトリガーフローの開始設定

開始要素の次に「割り当て」を追加し、$Record.Description01: before-save Flowを設定します。

before-save Flowで取引先の説明を更新する割り当て要素

5.after-saveレコードトリガーフローを作成する

2つ目は、取引先の保存後に動くafter-saveフローです。開始条件はbefore-save Flowと同じですが、最適化の対象は「アクションと関連レコード」にします。

オブジェクト取引先
トリガーレコードが更新された
エントリ条件取引先名/変更済み/True
最適化アクションと関連レコード

開始条件に「取引先名が変更された」を指定するのがポイントです。2回目は説明だけが変わるため、after-save Flowは再度起動せず、同じフローが繰り返されることを防げました。

取引先名の変更後に動くafter-saveレコードトリガーフローの開始設定

「レコードを更新」要素でトリガー元の取引先を指定し、説明04: after-save Flowを設定します。これが、同じ取引先に対する2回目の更新です。

after-save Flowでトリガー元の取引先を再更新するレコード更新要素

6.取引先を更新してデバッグログを取得する

2つのFlowを有効化し、Apexトリガーも有効な状態で取引先を開きます。デバッグログの対象ユーザーを設定したあと、取引先名を変更して保存します。

  1. 検証用の取引先を開く
  2. 変更前の説明を確認する
  3. 取引先名だけを変更して保存する
  4. 保存後の説明を確認する
  5. デバッグログを開き、ORDER_TESTで検索する

取引先名の変更前:

FlowとApexの実行順序を確認するため取引先名を変更する画面(変更前)

取引先名の変更後:

FlowとApexの実行順序を確認するため取引先名を変更する画面(変更後)

検証結果:値の上書きとApexの再実行を確認する

検証結果

before-save Flowとafter-save Flowはそれぞれ1回、before/after update Apexはそれぞれ2回動きました。最終的な説明は、after-save Flowが設定した04: after-save Flowでした。

after-save Flowによる同一レコードの更新でApexトリガーが再実行される流れ

実際のORDER_TESTのログは、次の順番になりました。

ORDER_TEST | before update #1 | Name changed=true | Description=01: before-save Flow
ORDER_TEST | after update #1 | Description=02: before update Apex
ORDER_TEST | before update #2 | Name changed=false | Description=04: after-save Flow
ORDER_TEST | after update #2 | Description=04: after-save Flow

1回目のbefore update処理では、トリガーハンドラーが01: before-save Flowを受け取っていました。ここから、before-save Flowが先に動いたことが分かります。その後、ハンドラーが02: before update Apexへ上書きし、最初のafter update処理でも同じ値を確認できました。

after-save Flowが04: after-save Flowへ更新すると、同じ取引先に対して保存処理がもう一度始まりました。2回目に呼び出されたトリガーハンドラーのログはName changed=falseDescription=04: after-save Flowとなり、Apexだけが再実行された流れを確認できました。

before updateとafter updateのApexトリガーが2回ずつ動いたデバッグログ
FlowとApexによる更新後に取引先の説明へ残った最終値
順番処理確認できた値・結果
1before-save Flow01: before-save Flowを設定
21回目のbefore update Apex01を受け取り、02へ上書き
31回目のafter update Apex02: before update Apex
4after-save Flow04: after-save Flowへ再更新
52回目のbefore update Apex04: after-save Flow。取引先名は変更されていないため上書きなし
62回目のafter update Apex04: after-save Flow

after-save Flowで同じレコードを更新するときの注意点

今回の検証では、after-save Flowから同じレコードを更新すると、保存処理がもう一度動くことを確認できました。起動条件が広いままだと、FlowやApexが意図せず繰り返し実行される可能性があります。

  • 開始条件を、業務上必要な項目の変更時だけに絞る
  • Flowでは$Record$RecordPrior、ApexではTrigger.newTrigger.oldMapを比較する
  • 同じレコードの項目更新だけなら、before-save Flowまたはbefore Apexで完結できないか確認する
  • 同じオブジェクト上のFlowとApexを横断して、どの処理が再実行されるか確認する

Salesforceの公式アーキテクチャガイドでも、同じレコードの項目更新にはbefore-saveを使い、不要な2回目のDMLと再帰的な保存処理を避ける考え方が示されています。after-saveで同じレコードを更新する必要があるかは、最初に見直したいところです。

再実行によって複数の処理が積み上がると、SOQLやDMLなどのガバナ制限にも影響します。ログから処理の積み上がりを確認する方法は、次の関連記事で詳しく確認しています。

FlowとApexが同じオブジェクトにある場合は設計も確認する

今回のようにFlowとApexを混在させる構成は、あくまで動きを確認するためのものです。実運用では、同じオブジェクトにレコードトリガーフローとApexトリガーを別々の入口として作るのは原則避けたいと考えています。入口が増えるほど、値の上書き、再実行、ガバナ制限への影響を横断して追う必要があるためです。

同じオブジェクトではFlowかApexのどちらかに自動化の入口を寄せる設計方針

今回のような混在構成は挙動を確認するためのものです。実運用では、保守性や実行順序の分かりやすさを考え、同じオブジェクトの自動化の入口をFlowまたはApexのどちらかに寄せるのが基本です。

Salesforceの公式アーキテクチャガイドでも、1つのオブジェクトでは自動化の入口を1つにし、同じオブジェクトでFlowとApexトリガーを入口として混在させないよう案内されています。Flowで無理なく保守できる処理はFlowへ、複雑度やデータ量、細かなトランザクション制御からApexが適する処理はApexへ寄せます。Flowを入口にして複雑な処理だけInvocable Apexへ任せる構成も選択肢です。

まとめ

この記事では、before-save Flow、before/after Apexトリガー、after-save Flowから同じ取引先の説明を更新し、値の上書きと再実行を実際に確認しました。

  • before Apexは、before-save Flowが設定した01を受け取り、02へ上書きした
  • 1回目のafter Apexでは、before Apexが上書きした02を確認できた
  • after-save Flowが同じ取引先を更新すると、before/after Apexが2回目も起動した
  • 最終的な説明には、after-save Flowが設定した04が残った
  • 実運用では1オブジェクトにつき自動化の入口を1つにし、FlowトリガーとApexトリガーの混在は原則避ける

実行順序だけを見ると単純ですが、after-save Flowが同じレコードを更新すると、Apexは同じトランザクション内で再実行されます。今回のように更新対象と開始条件を絞り、2回目にどの処理が動くかまでデバッグログで確認しておくと、値の上書きや意図しない再帰を追いやすくなります。

参考リンク

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

ちゃあむのアバター ちゃあむ エンジニア

Web開発やSaaS(ServiceNow、Salesforce)、業務システムに携わる、猫と食べることが大好きなインドア系ITエンジニアです。

コメント

コメントする


目次