Salesforce ロイヤリティプログラム、Flowで頑張りすぎた管理者の後悔

loyalty program で実際にやらかした判断

当時の私は、Salesforceの管理者として、新しいロイヤリティプログラムの構築を任されました。「できるだけ標準機能で」という要件もあり、私は「Flowでどこまでやれるか」を試してしまったんです。結果的に、それが大きな後悔になりました。

最初の段階でやらかしたのは、ポイント付与と会員ランク判定のロジックを、完全にLightning Flow (主にRecord-Triggered FlowとScheduled-Triggered Flow) で完結させようとしたことです。

ポイント付与ロジックにおける判断ミス

当時の要件はシンプルでした。「商品購入があったら、購入金額に応じてポイントを付与する」。これはRecord-Triggered Flowで容易に実現できました。

  • 購入金額が100円につき1ポイント
  • 特定のプロモーション期間中はポイント2倍

これを実現するために、

// OrderオブジェクトにRecord-Triggered Flowを設定
// 実行タイミング: レコード作成時、または更新時(購入ステータスが「完了」になった場合)
// エントリ条件: Order.Status = 'Completed'
// アクション: PointTransactionオブジェクトにレコードを作成
//            PointTransaction.Amount = FLOOR(Order.TotalAmount / 100) * (IF(Order.IsPromotion__c, 2, 1));
のようなロジックをFlowの数式や割り当て要素で組んでいました。この部分は、今でもそこまで大きな問題だとは思っていません。単純な処理にはFlowは非常に強力です。

会員ランク判定ロジック、その破綻への道

本当に「やらかした」と感じたのは、会員ランクの判定ロジックでした。

要件はこうです。

  • 累積ポイントに基づいて会員ランクを決定(ブロンズ、シルバー、ゴールドなど)
  • 月に一度、全会員のランクを更新する
  • ランク降格も考慮する

私はこれをScheduled-Triggered Flowで実装しようとしました。毎月1日の深夜にFlowを起動し、全顧客レコードをループして、それぞれに対して累積ポイントを計算し、ランクを更新するという設計です。

当時の私の判断としては、「管理者で完結できる」「Apex開発のコストをかけたくない」という思いが強くありました。少数の顧客であれば、このやり方でも問題ないだろう、と甘く考えていたのです。

直面したGovernor Limitsの壁

顧客数が数百件を超え、ポイント取引レコードが数万件に達したあたりで、問題は顕在化しました。

Scheduled-Triggered Flowは、対象レコードをバッチ処理するとはいえ、ループ内で関連レコード(PointTransaction)をSOQLでクエリするたびに、Governor Limitsに抵触し始めました。

// Scheduled-Triggered Flowの内部ロジックイメージ
// 1. Customerオブジェクトの全レコードをループ (これはFlowのバッチ処理で分割されるが...)
// 2. ループ内で、現在のCustomer IDを元にPointTransactionレコードをSOQLで検索
//    GET RECORDS: PointTransaction (WHERE CustomerId = CurrentCustomer.Id AND CreatedDate >= LAST_N_MONTHS:12)
// 3. 検索結果をループして合計ポイントを計算
// 4. 合計ポイントに基づいてCustomer.MemberRank__cを更新

この「2. ループ内でSOQL」が致命傷でした。例えば、50,000件の顧客がいる場合、Flowはこれを200件ずつ程度のバッチで処理しますが、各バッチ内でさらにSOQLクエリを顧客数分発行しようとすると、すぐに「Total number of SOQL queries issued: 101」というエラーにぶち当たります。

顧客の累積ポイントを計算するために、関連リストから全PointTransactionレコードを取得し、集計するといった処理は、Flowが最も苦手とする領域の一つだと痛感しました。特に、動的な期間指定(例:過去12ヶ月の購入に基づくランク)が入ると、事態はさらに悪化します。

後から「やらなければよかった」と思った設計

あの時、もしApex開発リソースが潤沢にあったり、私がもう少し将来を見据えることができていれば、間違いなくScheduled-Triggered Flowで全件ループによるランク更新は選択しませんでした。

今なら、間違いなくApex Batch Jobで実装します。Batch Apexであれば、より効率的に大量のデータを処理し、SOQLクエリの最適化も可能です。たとえば、全ての顧客のIDと関連するポイント取引のサマリーを一度に取得し、メモリ上で処理するといったアプローチが可能です。

具体的には、 [SELECT Id, (SELECT Id, Amount FROM PointTransactions__r) FROM Customer__c] のようなクエリを安易に全件取得すると、これもガバナ制限の「Total number of records retrieved by SOQL queries」に抵触する可能性があります。むしろ、

// 今ならこう書く (Apex Batchのexecuteメソッド内)
// Map customerPointsMap = new Map();
// for (AggregateResult ar : [SELECT Customer__c Id, SUM(Amount) totalPoints FROM PointTransaction__c GROUP BY Customer__c]) {
//     customerPointsMap.put((Id)ar.get('Id'), (Decimal)ar.get('totalPoints'));
// }
//
// List customersToUpdate = new List();
// for (Customer__c cust : [SELECT Id, MemberRank__c FROM Customer__c WHERE Id IN :customerPointsMap.keySet()]) {
//     Decimal totalPoints = customerPointsMap.get(cust.Id);
//     // totalPointsに応じてMemberRank__cを判定・設定
//     // ...
//     customersToUpdate.add(cust);
// }
// update customersToUpdate;
のように、集計クエリを先に実行し、効率的にマップに格納してから顧客レコードを更新する設計にするでしょう。当時の私には、そこまでの知識と経験が不足していました。

そして、最も後悔しているのは、Flowで実装したロジックが複雑になりすぎた結果、後からApexへの移行が必要になった時、既存のFlowの処理を理解し、それをApexのコードに変換する手間が非常に大きかったことです。結局、二度手間、いや三度手間以上のコストがかかりました。

管理者として「自力でなんとかしたい」という気持ちはとてもよくわかります。しかし、スケーラビリティや将来的な複雑性を見据え、時には専門家(開発者)に協力を仰ぐ、あるいは最初から適切なツール(Batch Apex)を選択する勇気も必要だと痛感した一件でした。

これは当時の自分向けのメモだ。

コメント