Salesforce ケース管理の自動化を Apex トリガーで極める:開発者向けガイド

背景と適用シナリオ

Salesforce Service Cloud の中核をなすのは、Case Management (ケース管理) です。これは、顧客からの問い合わせ、問題、リクエストを「ケース」というオブジェクトで一元管理し、その解決プロセスを追跡・最適化するための機能群です。標準機能であるWeb-to-CaseEmail-to-CaseAssignment Rules (割り当てルール)、そして Escalation Rules (エスカレーションルール) などを駆使することで、多くの業務プロセスは自動化できます。

しかし、ビジネスが複雑化するにつれて、標準の宣言的ツールだけでは対応しきれない要件が出てきます。例えば、以下のようなシナリオが考えられます。

  • 特定のキーワードが件名に含まれ、かつ関連取引先の年間売上が一定額以上の場合にのみ、ケースの優先度を「高」に設定し、特定のキューに割り当てる。
  • ケースが作成された際、外部のプロジェクト管理システム(例: Jira)に API 経由でチケットを自動作成し、そのIDをSalesforceのケースに記録する。
  • ケースの種別が「技術サポート」で、製品ファミリーが「A」の場合、関連するナレッジ記事を自動で検索し、ケースに添付する。
  • ケースがクローズされる際、関連する作業指示(Work Order)オブジェクトのステータスも連動して更新する。

このような複雑なビジネスロジック、外部システム連携、または複数のオブジェクトにまたがるデータ操作は、宣言的なツールの限界を超えることが多く、Apex Trigger (Apex トリガー) を利用したプログラムによるカスタマイズが必要となります。本記事では、Salesforce 開発者の視点から、Apex トリガーを用いてケース管理プロセスをいかに高度に自動化し、業務効率を飛躍的に向上させるかについて、具体的なコードを交えながら解説します。


原理説明

Apex トリガーは、Salesforce のレコード(ここではケース)が作成、更新、削除されるといった特定のイベント(DML 操作)が発生する前後に、カスタムの Apex コードを自動的に実行するための仕組みです。トリガーを理解する上で重要な概念がいくつかあります。

トリガーイベント (Trigger Events)

トリガーは、特定のタイミングで起動するように設定します。主なイベントは以下の通りです。

  • before insert: レコードがデータベースに保存される前に実行。
  • before update: レコードが更新される前に実行。
  • before delete: レコードが削除される前に実行。
  • after insert: レコードが保存された直後に実行。
  • after update: レコードが更新された直後に実行。
  • after delete: レコードが削除された後、ごみ箱に入る前に実行。
  • after undelete: レコードがごみ箱から復元された後に実行。

例えば、「before insert」イベントでは、入力された値を検証したり、特定の項目値を自動で設定したりするのに適しています。「after insert」イベントでは、作成されたレコードの ID を使って、関連する別オブジェクトのレコード(タスクなど)を作成する処理に適しています。

トリガーコンテキスト変数 (Trigger Context Variables)

トリガーの実行中、どのレコードがどのような操作を受けているかといったコンテキスト情報にアクセスするための変数群が提供されています。ケース管理で特によく使うのは以下の変数です。

  • Trigger.new: 挿入または更新された新しいバージョンのレコードリスト。before insert, before update, after insert, after update で利用可能。
  • Trigger.old: 更新または削除される前の古いバージョンのレコードリスト。before update, before delete, after update, after delete で利用可能。
  • Trigger.newMap: レコードIDをキー、新しいバージョンのレコードを値とするMap。
  • Trigger.oldMap: レコードIDをキー、古いバージョンのレコードを値とするMap。
  • Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete: 現在の操作が挿入、更新、削除のいずれであるかを示す boolean 値。
  • Trigger.isBefore, Trigger.isAfter: トリガーが before イベントか after イベントかを示す boolean 値。

これらの変数を組み合わせることで、「更新前と後で優先度が変更された場合」といった特定の条件分岐を精密に記述できます。

トリガーハンドラーパターン (Trigger Handler Pattern)

ベストプラクティスとして、トリガーファイル自体にはロジックを記述せず、ロジックを別の Apex クラス(ハンドラークラス)に委譲する「トリガーハンドラーパターン」が推奨されます。これにより、コードの可読性、再利用性、テストのしやすさが向上します。トリガーは単なるディスパッチャー(司令塔)となり、実際の処理はハンドラークラスが担います。


示例代码

ここでは、ケースが作成された際に特定の条件に基づいて処理を自動化するシナリオを考えます。

シナリオ:

  1. ケースの件名 (Subject) に「緊急」または「Urgent」という単語が含まれている場合、そのケースの優先度 (Priority) を自動的に「High」に設定する。(Before Insert
  2. ケースが正常に作成された後、ケースの所有者に対して「新規ケースの確認と初期対応」という件名の ToDo (Task) を自動で作成する。(After Insert

このシナリオをトリガーハンドラーパターンで実装します。

1. トリガー本体 (CaseTrigger.trigger)

まず、Case オブジェクトに対するトリガーを作成します。このトリガーは、イベントを検知してハンドラークラスの適切なメソッドを呼び出すだけのシンプルなものです。

/*
 * CaseTrigger.trigger
 * このトリガーは Case オブジェクトのイベントを処理し、
 * すべてのロジックを CaseTriggerHandler クラスに委譲します。
 */
trigger CaseTrigger on Case (before insert, after insert) {
    // 新しいインスタンスを作成するのではなく、シングルトンパターンなどでハンドラーを管理することも可能です
    CaseTriggerHandler handler = new CaseTriggerHandler();

    // before insert イベントのコンテキストで実行
    if (Trigger.isBefore && Trigger.isInsert) {
        handler.onBeforeInsert(Trigger.new);
    }
    
    // after insert イベントのコンテキストで実行
    if (Trigger.isAfter && Trigger.isInsert) {
        handler.onAfterInsert(Trigger.new);
    }
}

2. トリガーハンドラークラス (CaseTriggerHandler.cls)

次に、実際の処理ロジックを記述するハンドラークラスを作成します。ロジックがメソッド単位で分割されているため、管理とテストが容易になります。

/*
 * CaseTriggerHandler.cls
 * CaseTrigger から呼び出され、ケースに関するビジネスロジックを実行します。
 * 1つのメソッドが1つの責務を持つように設計されています。
 */
public class CaseTriggerHandler {

    /**
     * @description ケースが作成される前に実行されるロジック
     * @param newCases トリガーによって処理される新しいケースのリスト (Trigger.new)
     */
    public void onBeforeInsert(List<Case> newCases) {
        // 件名に基づいて優先度を設定するロジック
        this.setPriorityBasedOnSubject(newCases);
    }

    /**
     * @description ケースが作成された後に実行されるロジック
     * @param newCases トリガーによって処理された新しいケースのリスト (Trigger.new)
     */
    public void onAfterInsert(List<Case> newCases) {
        // フォローアップタスクを作成するロジック
        this.createFollowUpTasks(newCases);
    }

    // --- Private Helper Methods ---

    /**
     * @description 件名に特定のキーワードが含まれている場合、優先度を 'High' に設定します。
     * @param casesToProcess 処理対象のケースリスト
     */
    private void setPriorityBasedOnSubject(List<Case> casesToProcess) {
        // ループ内で SOQL や DML を実行しない Bulk 対応の設計
        for (Case c : casesToProcess) {
            // Subject が null でないことを確認
            if (String.isNotBlank(c.Subject)) {
                // 件名に「緊急」または「Urgent」(大文字小文字を区別しない) が含まれているかチェック
                if (c.Subject.toLowerCase().contains('緊急') || c.Subject.toLowerCase().contains('urgent')) {
                    c.Priority = 'High'; // 優先度を 'High' に設定
                }
            }
        }
    }

    /**
     * @description 新規作成された各ケースに対してフォローアップタスクを作成します。
     * @param createdCases 作成されたケースのリスト
     */
    private void createFollowUpTasks(List<Case> createdCases) {
        List<Task> tasksToInsert = new List<Task>();
        
        // for ループを使って各ケースに対するタスクを準備
        for (Case c : createdCases) {
            Task t = new Task();
            t.Subject = '新規ケースの確認と初期対応';
            t.OwnerId = c.OwnerId; // タスクの所有者をケースの所有者と同じにする
            t.WhatId = c.Id;       // WhatId にケースのIDを関連付け、タスクをケースにリンクさせる
            t.ActivityDate = System.today().addDays(1); // 期日を明日に設定
            t.Status = 'Not Started';
            t.Priority = 'Normal';
            tasksToInsert.add(t);
        }

        // Governor Limit を避けるため、リストに溜めたタスクを一括で挿入 (DML の Bulk 化)
        if (!tasksToInsert.isEmpty()) {
            try {
                insert tasksToInsert;
            } catch (DmlException e) {
                // エラーハンドリング: 例としてデバッグログに出力
                System.debug('タスクの作成に失敗しました: ' + e.getMessage());
                // ここでカスタムのエラーロギング処理を実装することも可能
            }
        }
    }
}

コードのポイント:

  • Bulkification (一括処理): コードは常に複数のレコードが一度に処理されることを想定して書かれています。for ループ内で DML 操作 (insert, update) を行わず、リストに溜めてからループの外で一度に実行することで、Governor Limits (ガバナ制限) に抵触するリスクを大幅に低減します。
  • 責務の分離: setPriorityBasedOnSubjectcreateFollowUpTasks というように、ロジックが関心事ごとにプライベートメソッドに分割されており、コードが読みやすく、保守しやすくなっています。
  • エラーハンドリング: DML 操作は try-catch ブロックで囲むことで、一部のレコードでエラーが発生してもトランザクション全体が失敗するのを防いだり、エラーログを記録したりといった堅牢な処理が可能になります。

注意事項

ガバナ制限 (Governor Limits)

Salesforce はマルチテナント環境であるため、1つの組織の処理が他の組織に影響を与えないよう、1回のトランザクションで実行できる処理量に厳しい制限(ガバナ制限)を設けています。トリガー開発において特に注意すべきは以下の制限です。

  • SOQL クエリの発行回数: 同期処理では100回まで。
  • DML ステートメントの発行回数: 150回まで。
  • CPU 実行時間: 10,000ミリ秒まで。

前述の Bulkification は、これらの制限を遵守するための最も基本的なテクニックです。

再帰的トリガー (Recursive Trigger)

トリガー内のロジックが、同じトリガーを再度起動するようなレコード更新を行うと、無限ループ(再帰)に陥る可能性があります。例えば、ケースを更新するトリガー内で、そのケース自身を再度更新するようなコードがあると危険です。これを防ぐには、static 変数を用いたフラグを立て、トリガーが一度実行されたら同じトランザクション内では再実行されないように制御する手法が一般的です。

public class TriggerControl {
    public static boolean hasCaseTriggerRun = false;
}

// トリガーの先頭でチェック
if (TriggerControl.hasCaseTriggerRun) {
    return;
}
TriggerControl.hasCaseTriggerRun = true;
// ... トリガーロジック ...

テストコードの重要性

本番環境に Apex コードをデプロイするには、そのコードに対するテストクラスを作成し、75% 以上のコードカバレッジを達成する必要があります。テストコードは、単にカバレッジを満たすためだけでなく、実装したロジックが期待通りに動作することを保証し、将来の変更による意図しない不具合(リグレッション)を防ぐために不可欠です。


まとめとベストプラクティス

Apex トリガーは、Salesforce の標準機能だけでは実現不可能な、複雑で動的なケース管理プロセスの自動化を可能にする強力なツールです。宣言的なツールで対応できるものはそちらを優先し、それが限界に達したときに初めて Apex の利用を検討するのが基本姿勢ですが、一度利用すると決めたからには、その能力を最大限に引き出すためのベストプラクティスを遵守することが重要です。

ケース管理における Apex トリガー開発のベストプラクティス:

  1. One Trigger Per Object: 1つのオブジェクトには1つのトリガーのみを作成する。これにより、実行順序が予測不能になる事態を防ぎます。
  2. ロジックをトリガーから分離する: トリガーハンドラーパターンを採用し、トリガーはディスパッチャーの役割に徹させます。
  3. コードの一括処理 (Bulkify): 常に複数のレコードが処理されることを前提にコードを記述します。
  4. コンテキスト変数を効率的に利用する: Trigger.newMapTrigger.oldMap を活用し、ループ内での不要なクエリを避けます。
  5. 再帰を制御する: static 変数などを利用して、意図しないトリガーの再実行を防ぎます。
  6. 網羅的なテスト: ポジティブケース、ネガティブケース、そして一括処理のシナリオをカバーするテストクラスを作成します。

これらの原則に従うことで、あなたはスケーラブルで保守性が高く、かつ堅牢なケース管理自動化システムを構築できる Salesforce 開発者となることができるでしょう。顧客サービスの品質とエージェントの生産性を同時に向上させる、価値あるソリューションの提供が可能になります。

コメント