背景と適用シナリオ
Salesforce 開発者として、私たちは常にビジネスプロセスの自動化と最適化を求められています。特に、Service Cloud の中核をなすケース管理 (Case Management) は、顧客満足度に直結する重要な領域です。Salesforce には標準でケース割り当てルール (Case Assignment Rules) やエスカレーションルール (Escalation Rules) が用意されていますが、複雑なビジネス要件には対応しきれない場面が少なくありません。
例えば、以下のようなシナリオを考えてみましょう。
- スキルベースのルーティング: ケースの内容(製品カテゴリ、言語、問題の技術的な複雑さなど)を分析し、最も専門知識を持つ適切なスキルセットを持ったサポート担当者に自動で割り当てる。
- ラウンドロビン割り当て: 特定のチーム内で、各担当者に均等にケースが分配されるように、順番に割り当てる。
- 動的な優先度設定と SLA 管理: 特定のキーワードがケースの件名や説明に含まれていた場合、自動的に優先度を「High」に変更し、関連するSLA(サービスレベルアグリーメント)タイマーを起動する。
- 関連レコードに基づく割り当て: ケースが関連する取引先 (Account) の所有者や、特定の契約 (Contract) を担当するチームにケースを割り当てる。
これらの要件は、標準機能のルール設定だけでは実現が困難です。このような状況で強力な武器となるのが、Salesforce のサーバーサイドプログラミング言語である Apex です。Apex Trigger を使用することで、ケースの作成時や更新時に独自のビジネスロジックを実装し、上記のような高度な自動化を実現することが可能になります。
この記事では、Salesforce 開発者の視点から、Apex を活用してケースの割り当てとエスカレーションプロセスを自動化する方法について、具体的なコード例を交えながら詳しく解説します。
原理説明
Apex を用いたケース管理自動化の核となるのは、Apex Trigger(エイペックストリガー)です。Trigger は、データベース上で特定のイベント(レコードの挿入、更新、削除など)が発生した際に自動的に実行されるコードブロックです。
ケースオブジェクトに対して Trigger を設定することで、ケースが作成されたり、内容が変更されたりするタイミングを捉え、カスタムロジックを介入させることができます。
トリガーコンテキスト変数 (Trigger Context Variables)
Trigger 内では、処理対象のレコードにアクセスするための特殊な変数、いわゆるコンテキスト変数が提供されます。ケース管理で特によく使用するものは以下の通りです。
- Trigger.new: 挿入 (insert) または更新 (update) された新しいバージョンのレコードリスト。このリスト内のレコードの項目値を変更することで、データベースに保存される前のデータを操作できます(before a trigger)。
- Trigger.oldMap: 更新 (update) 前の古いバージョンのレコードを ID をキーとして保持する Map。更新前後の値の比較に使用します(例:「優先度が『Medium』から『High』に変更されたか?」)。
- Trigger.isInsert, Trigger.isUpdate: Trigger が insert 操作によって起動したか、update 操作によって起動したかを示す boolean 値。
- Trigger.isBefore, Trigger.isAfter: Trigger がレコードの保存前に実行されているか、保存後に実行されているかを示す boolean 値。割り当てのようにレコード保存前に項目値を変更したい場合は `before` イベントを使用します。
自動化ロジックのフロー
高度なケース割り当てロジックを Apex Trigger で実装する際の一般的なフローは以下のようになります。
- トリガーの起動: ユーザーや自動化プロセスによってケースが作成または更新されると、Case オブジェクトに設定された `before insert` または `before update` の Trigger が起動します。
- 条件の評価: Trigger 内で、特定のロジックを適用すべきケースかどうかを判断します。例えば、「新規作成されたケース」や「優先度が変更されたケース」などを `Trigger.isInsert` や `Trigger.oldMap` を使って判定します。
- 関連情報の取得: 必要に応じて、SOQL (Salesforce Object Query Language) を使用して、割り当て先の候補となるユーザーやキュー (Queue)、または判断材料となる他のオブジェクト(取引先、カスタムスキルオブジェクトなど)の情報をデータベースから取得します。
- 割り当てロジックの実行: 取得した情報とケースのデータに基づき、最適な所有者 (OwnerId) を決定します。これがスキルベースのマッチングやラウンドロビンのロジックの中核部分です。
- レコードの更新: 決定した所有者の ID を、`Trigger.new` のリストに含まれる対象ケースの `OwnerId` 項目に設定します。`before` Trigger を使用しているため、この変更はそのままデータベースに保存され、追加の DML (Data Manipulation Language) 操作(`update` ステートメント)は不要です。
このフロー全体を通して、一度のトランザクションで複数のレコードが処理される可能性を考慮した一括処理(バルク化、Bulkification)を意識することが、ガバナ制限 (Governor Limits) を回避する上で極めて重要です。
サンプルコード
ここでは、「ケースの種別 (Type) に応じて、対応する専門チーム(公開グループ)にラウンドロビン方式でケースを割り当てる」シナリオを想定した Apex Trigger とハンドラークラスの例を示します。このコードは Salesforce Developer の公式ドキュメントで示されているベストプラクティスに基づいています。
ベストプラクティス:ロジックを Trigger 本体に直接記述するのではなく、ハンドラークラスに分離することで、コードの再利用性、保守性、テストの容易性が向上します。
ステップ1: ラウンドロビン管理用のカスタムオブジェクト (任意)
厳密なラウンドロビンを実装する場合、最後に誰に割り当てたかを記録するオブジェクトがあると便利です。ここでは簡略化のため、グループメンバーを都度取得し、ランダムまたは単純な剰余計算で割り当てるロジックを採用します。
ステップ2: Apex ハンドラークラス (CaseTriggerHandler.cls)
実際の割り当てロジックをこのクラスに実装します。
public class CaseTriggerHandler { // ラウンドロビン割り当てロジックをカプセル化するための内部クラス private static Map<Id, Integer> groupMemberIndex = new Map<Id, Integer>(); /** * @description ケースが新規作成された際に、種別に基づいて適切なグループに割り当てる * @param newCases Trigger.new コンテキスト変数から渡される新規ケースのリスト */ public static void handleRoundRobinAssignment(List<Case> newCases) { // 処理対象のケース種別と、対応する公開グループ (Public Group) の開発者名をマッピング Map<String, String> typeToGroupDevName = new Map<String, String>{ 'Technical Support' => 'Tech_Support_Queue', 'Billing Inquiry' => 'Billing_Support_Queue', 'Product Feedback' => 'Product_Team_Queue' }; Set<String> groupDevNames = new Set<String>(); for (Case c : newCases) { // OwnerIdが未設定で、かつマッピングに存在する種別のケースのみを対象 if (c.OwnerId == null && typeToGroupDevName.containsKey(c.Type)) { groupDevNames.add(typeToGroupDevName.get(c.Type)); } } if (groupDevNames.isEmpty()) { return; // 処理対象がなければ終了 } // SOQLを使用して、必要な公開グループの情報を一度に取得(バルク化) Map<String, Group> groupMap = new Map<String, Group>(); for(Group g : [SELECT Id, DeveloperName FROM Group WHERE DeveloperName IN :groupDevNames AND Type = 'Queue']) { groupMap.put(g.DeveloperName, g); } // 各グループに所属するアクティブなユーザーを取得 Map<Id, List<User>> groupToUsersMap = new Map<Id, List<User>>(); for (GroupMember gm : [SELECT GroupId, UserOrGroupId FROM GroupMember WHERE GroupId IN :groupMap.values() AND UserOrGroupId IN (SELECT Id FROM User WHERE IsActive = true)]) { if (String.valueOf(gm.UserOrGroupId).startsWith('005')) { // ユーザーIDであることを確認 if (!groupToUsersMap.containsKey(gm.GroupId)) { groupToUsersMap.put(gm.GroupId, new List<User>()); } // ここではUserのIdのみを保持する(メモリ節約のため) // 実際のシナリオではUserオブジェクトを取得してもよい groupToUsersMap.get(gm.GroupId).add(new User(Id = gm.UserOrGroupId)); } } // ケースをループして所有者を割り当てる for (Case c : newCases) { if (c.OwnerId == null && typeToGroupDevName.containsKey(c.Type)) { String devName = typeToGroupDevName.get(c.Type); Group targetGroup = groupMap.get(devName); if (targetGroup != null && groupToUsersMap.containsKey(targetGroup.Id)) { List<User> potentialOwners = groupToUsersMap.get(targetGroup.Id); if (!potentialOwners.isEmpty()) { // 現在のインデックスを取得(初回は0) Integer currentIndex = groupMemberIndex.get(targetGroup.Id); if (currentIndex == null) { currentIndex = 0; } // ラウンドロビンロジック:次の担当者を決定 Id nextOwnerId = potentialOwners[currentIndex].Id; c.OwnerId = nextOwnerId; // ケースの所有者を設定 // 次回のためにインデックスを更新 groupMemberIndex.put(targetGroup.Id, (currentIndex + 1) % potentialOwners.size()); } } } } } }
ステップ3: Apex Trigger (CaseTrigger.trigger)
Case オブジェクトに対する Trigger を作成し、ハンドラークラスのメソッドを呼び出します。
trigger CaseTrigger on Case (before insert) { // トリガーのコンテキストに応じて、適切なハンドラーメソッドを呼び出す if (Trigger.isBefore) { if (Trigger.isInsert) { // 新規作成時、ラウンドロビン割り当てロジックを実行 CaseTriggerHandler.handleRoundRobinAssignment(Trigger.new); } } // 今後、更新時(isUpdate)や削除時(isDelete)のロジックもここに追加できる }
⚠️ このコードは Salesforce の公式ドキュメントにある Apex の基本的な構文、SOQL、DML の原則に基づいて構成されています。`Group` や `GroupMember` オブジェクトのクエリ方法は、Salesforce の標準的な開発手法です。
注意事項
Apex を使用した自動化を実装する際には、いくつかの重要な点に注意する必要があります。
権限 (Permissions)
Trigger は、操作を実行したユーザーのコンテキストで実行されます。したがって、Trigger を起動したユーザーは、ケースの `OwnerId` を変更する権限、および関連オブジェクト(`Group`, `User` など)への参照アクセス権を持っている必要があります。権限が不足している場合、Trigger はエラーとなり、レコードの保存は失敗します。
API 制限 (API and Governor Limits)
Salesforce はマルチテナント環境であるため、すべての組織がリソースを公平に利用できるように、1回のトランザクションで実行できる処理にはガバナ制限が設けられています。
- SOQL クエリ: 1トランザクションあたり100回まで。上記のコード例のように、ループ内で SOQL を発行するのは絶対に避け、一度のクエリで必要なデータをすべて取得するように設計する必要があります(`WHERE Id IN :idSet` のような形式)。
- DML ステートメント: 1トランザクションあたり150回まで。DML 操作もループ内で行わず、リストにまとめて一度に実行します。
- CPU 時間: 1トランザクションあたりの CPU 使用時間にも制限があります。複雑すぎるロジックや非効率なループは、この制限に抵触する可能性があります。
エラー処理 (Error Handling)
例えば、割り当て先のグループにアクティブなユーザーが一人もいなかった場合、コードは例外を投げる可能性があります。`try-catch` ブロックを使用して例外を捕捉し、管理者に通知する、またはデフォルトのキューに割り当てるなどのフォールバックロジックを実装することが重要です。
再帰的実行の防止 (Preventing Recursive Execution)
ケースを更新する Trigger が、別の自動化(例: Workflow Rule, Process Builder)によって再度同じケースを更新し、Trigger が無限に呼び出される「再帰ループ」に陥ることがあります。これを防ぐため、`static boolean` 変数を使用して、特定のトランザクション内で Trigger が一度しか実行されないように制御するデザインパターンが一般的に用いられます。
まとめとベストプラクティス
Apex を活用することで、標準機能では実現不可能な、動的でインテリジェントなケース管理の自動化が可能になります。これにより、サポートチームの効率を大幅に向上させ、顧客への対応時間を短縮し、最終的には顧客満足度の向上に繋がります。
Salesforce 開発者として、この種のカスタマイズを実装する際のベストプラクティスを以下にまとめます。
- 1オブジェクトに1つのトリガー (One Trigger Per Object): 1つのオブジェクトに対して複数の Trigger を作成すると、実行順序を制御できず、予期せぬ動作を引き起こす原因となります。すべてのロジックを1つの Trigger からハンドラークラスに振り分ける構成にしてください。
- ロジックはハンドラークラスへ (Logic-less Triggers): Trigger 本体にはロジックを記述せず、ハンドラークラスのメソッドを呼び出すだけにします。これにより、コードが整理され、テストや再利用が容易になります。
- 常にバルク化を意識する (Always Bulkify Your Code): コードは常に1件のレコードだけでなく、最大200件のレコードが一度に処理されることを想定して記述します。ループ内での SOQL や DML は厳禁です。
- ハードコーディングを避ける (Avoid Hardcoding IDs): 公開グループの ID などをコードに直接書き込むのではなく、`DeveloperName` を使用して動的に取得するか、カスタムメタデータ型 (Custom Metadata Types) やカスタム設定 (Custom Settings) を使用して、管理者がコードを変更せずに設定を更新できるように設計します。
- 十分なテストカバレッジ (Provide Full Test Coverage): Apex Trigger は、本番環境にデプロイする前に少なくとも75%のコードカバレッジを持つテストクラスを作成する必要があります。正常系だけでなく、異常系やバルク処理のシナリオも網羅したテストを記述することが、システムの安定性を保証します。
これらの原則に従うことで、スケーラブルで保守性の高い、堅牢なケース管理自動化ソリューションを構築することができるでしょう。
コメント
コメントを投稿