🥀

[Salesforce] フローのISNEW()にハマった

に公開

株式会社TERASSでSalesforceをやったりやらなかったりしているimslpです。

はじめに

皆さんはフローを使っていますか?
私は基本的に、レビューのしやすさや開発効率の高さからApexを選択することが多いです。
とはいえ、既存のフローのメンテナンスやフローの方が簡単である場合にフローを選択することもあります。
今回は、そうした既存フローのメンテナンス中にハマった事例を共有します。

トリガー構成

Apexトリガー

after insertで諸々の処理を実施し、最後にトリガーレコード自身を更新する。
他オブジェクトのレコード作成も含まれるため、beforeトリガーへの移行は難しい。
もともとはトリガーレコードを更新しない構成だったが、最近になって更新処理が追加された。
そのため、トリガーレコード更新を前提とした設計になっていない。

フロー

商談の作成/更新で動作する単一フロー
作成時の処理と更新時の処理が分かれており、分岐条件でISNEW()を利用している

何が起こったか

作成/更新トリガーのフローにおいて、
「更新時」の処理を実行しているにもかかわらずISNEW()trueになっていたという現象が発生しました。
https://help.salesforce.com/s/articleView?id=platform.customize_functions_isnew.htm&type=5

勘の鋭い方なら、もう原因が見えてきたかもしれません。

そうです「同一トランザクション内の再トリガー」が原因です。
まずは処理順序をおさらいします。かなりざっくり書くと、以下のようになっています。

  1. 項目の値を検証
  2. beforeトリガー実行
  3. 入力規則などのチェック
  4. DBに保存
  5. afterトリガー実行
  6. DBにコミット

問題となったのは5番です。
after insertの中で対象レコードを更新するDMLを行うと、同じトランザクション内で再度更新のトリガ処理を実行します(つまり作成コンテキスト内で更新が行われる)
ここでフローが再評価されますが、その際にフローで「このレコードは新規である(ISNEW() = true)」と判定されるわけです。

insert
 ├ before insert
 ├ validation rules
 ├ save
 ├ after insert  ← ここで更新
 │      ↓
 │   update(再トリガ)
 └ commit

どう対応するか

いくつか策は考えられました。

1. フローの条件を工夫する

例えば変更前レコード(Record__Prior)を参照する方法です。
これも再トリガの影響なのか、期待通り動作しなかったため諦めました。

2. フローの作りを変更する

作成/更新トリガーのフローを作成と更新に分割する方法です。
必要なものを必要なときに動かすという設計です。

3. トリガレコードを更新しない設計にする

beforeトリガでトリガレコードの項目に値を入れるというやり方です。
トリガレコードに変更を加える場合はこれがベストな選択だと思います。

4. トリガレコードの更新を非同期に逃がす

非同期処理にすることで更新を別トランザクションにする方法です。
処理の内容によって非同期にできるケースとできないケースがあると思います。

今回実際に行った対応

最終的には「トリガレコードの更新を非同期に逃がす」方法を採用しました。
これにより別トランザクションでの更新となり、作成コンテキストから外れるためISNEW()がtrueになることはなくなります。

after insert内のupdate箇所をそのままfutureメソッドに移行し、フロー側の作りや条件などはそのままにしています。
より良い選択は他にありましたが、やりたいことに対して一番ちょうどよかったため非同期処理を利用しました。

insert
 ├ before insert
 ├ validation rules
 ├ save
 ├ after insert  ← ここで非同期更新
 └ commit
↓
update(非同期更新のトランザクション)
 ├ before update
 ├ validation rules
 ├ save
 ├ after update
 └ commit

選ばれなかった選択肢

フローの条件を工夫する

Record__Priorを使った判定に切り替える案もありましたが、期待したロジックにならなかったため断念しました。

フローの作りを変更する

本来は「作成時だけ動くフロー」と「更新時だけ動くフロー」「作成時/更新時に動くフロー」にちゃんと分けるのが理想のはずです。
しかし今回は既存フローがかなり肥大化しており、このタイミングで大幅に構造を変えるとリスクが大きいため見送りました。

トリガレコードを更新しない設計にする

おそらくベストプラクティスは「トリガレコードの変更はbeforeトリガで行う」ことです。
ただし今回は、トリガレコードを元に関連レコードを作成・更新する処理が多数あり、設計全体を見直す必要があるレベルで影響が大きかったため選択しませんでした。

まとめ

今回の問題は、afterトリガー内でトリガレコードを更新してしまったことが根本原因であり、非同期処理へ逃がすことで一時的に回避できたものの、綺麗な解決方法とは言えませんでした。

非同期処理にするのであれば、データ更新だけでなく入口部分から一貫して非同期化しても良かったですし、今回のように部分的に非同期へ逃がす形は設計として中途半端だったと感じています。

結果として、

  • afterトリガーでトリガレコードを更新しない
  • トリガー設計をしっかり行う
  • フローと Apex が混在する場合はデータの流れが不透明になるため、役割分担を明確にする

といった基本を疎かにしたことが影響したように思います。
今後はフローとApexの使い分けを明確にし、トリガー全体の設計を見直すことで、同様の問題を防止していきたいですね。

ご覧いただきありがとうございました。

Terass Tech Blog

Discussion