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)を選択する勇気も必要だと痛感した一件でした。
これは当時の自分向けのメモだ。
コメント
コメントを投稿