Salesforce Apex コールアウトのマスターガイド:外部サービス連携の完全解説

こんにちは、Salesforce 統合エンジニア (Salesforce Integration Engineer) です。Salesforce を企業のハブとして活用する上で、外部システムとの連携は避けては通れない重要なテーマです。今回は、その連携の核となる技術の一つ、Apex Callouts について、その基本からベストプラクティスまでを詳しく解説していきます。


背景と応用シーン

Salesforce は強力なプラットフォームですが、それ単体で全てのビジネス要件を満たすわけではありません。企業のIT環境には、ERP、会計システム、在庫管理システム、マーケティングオートメーションツールなど、様々な専門システムが既に存在しています。Apex Callouts は、これらの外部システムと Salesforce をプログラム的に連携させるための仕組みです。

具体的には、Apex コード内から外部の Web サービス(通常は RESTful API や SOAP API)を呼び出し、データの送受信を行う機能です。これにより、Salesforce のデータと外部システムのデータを同期させたり、外部の高度な機能を Salesforce のプロセスに組み込んだりすることが可能になります。

応用シーンの例:

  • 住所情報の補完: 郵便番号から外部の住所検索 API を呼び出し、都道府県や市区町村を自動入力する。
  • 決済処理: 商談が「成立」になったタイミングで、Stripe や PayPal などの決済ゲートウェイ API を呼び出し、請求処理を自動化する。
  • 在庫確認: Salesforce の商品オブジェクトから、リアルタイムで外部の在庫管理システムの API を叩き、最新の在庫数を取得・表示する。
  • データ連携: Salesforce で作成された取引先や注文情報を、夜間バッチで基幹システム(ERP)に送信する。

このように、Apex Callouts は Salesforce の機能を拡張し、ビジネスプロセスをシームレスに自動化するための鍵となります。

原理説明

Apex Callout の基本的な仕組みは、Apex の標準クラスを使用して HTTP リクエストを作成し、外部のエンドポイントに送信し、レスポンスを受け取るという流れです。このプロセスは、主に3つの組み込みクラスによって実現されます。

  • HttpRequest: 送信する HTTP リクエストを表現するクラスです。エンドポイント URL、HTTP メソッド(GET, POST, PUT, DELETE など)、ヘッダー情報、リクエストボディなどを設定します。
  • HttpResponse: 外部サービスから返ってきた HTTP レスポンスを表現するクラスです。ステータスコード(200 OK, 404 Not Found など)、レスポンスボディ、ヘッダー情報を取得できます。
  • Http: 実際にリクエストを送信するためのクラスです。send() メソッドに HttpRequest オブジェクトを渡すことで、同期的にコールアウトを実行し、HttpResponse オブジェクトを返します。

この一連の流れは、Web ブラウザが Web サーバーと通信する仕組みと非常によく似ています。Apex コードがクライアントの役割を果たし、外部の Web サービスがサーバーの役割を果たします。

また、コールアウトには同期的 (Synchronous) な処理と非同期的 (Asynchronous) な処理があります。トリガーや Visualforce ページコントローラーの直接の処理など、即時のレスポンスが必要な場合は同期コールアウトが使われますが、処理に時間がかかる場合や、ガバナ制限 (Governor Limits) を回避したい場合には @future アノテーションや Queueable Apex を使用した非同期コールアウトが推奨されます。

サンプルコード

ここでは、Salesforce の公式ドキュメントで紹介されているコードを基に、具体的な実装例を見ていきましょう。

例1: REST GET コールアウト (動物情報の取得)

この例では、外部の公開 API を呼び出して動物の情報を取得するシンプルな GET リクエストを実装します。コールアウトを行うクラスと、それをテストするためのクラスが含まれています。

Apex クラス: AnimalLocator

// public class AnimalLocator {
//    public static String getAnimalNameById(Integer id) {
//        Http http = new Http();
//        HttpRequest request = new HttpRequest();
//        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals/' + id);
//        request.setMethod('GET');
//        HttpResponse response = http.send(request);
//        // If the request is successful, parse the JSON response.
//        if (response.getStatusCode() == 200) {
//            // Deserializes the JSON string into collections of primitive data types.
//            Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
//            // Cast the values in the 'animal' key as a map
//            Map<String, Object> animal = (Map<String, Object>) results.get('animal');
//            System.debug('Received the following animal name: ' + animal.get('name'));
//            return (String) animal.get('name');
//        }
//        return null;
//    }
// }

public class AnimalLocator {
    /**
     * @description 指定されたIDの動物名を取得するメソッド
     * @param id 動物のID
     * @return 動物の名前(String)。見つからない場合はnull。
     */
    public static String getAnimalNameById(Integer id) {
        // 1. Httpオブジェクトのインスタンスを作成
        Http http = new Http();

        // 2. HttpRequestオブジェクトのインスタンスを作成
        HttpRequest request = new HttpRequest();
        // エンドポイントURLを設定。IDを動的に組み込む
        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals/' + id);
        // HTTPメソッドをGETに設定
        request.setMethod('GET');

        // 3. リクエストを送信し、HttpResponseオブジェクトを受け取る
        HttpResponse response = http.send(request);

        // 4. レスポンスを処理する
        // ステータスコードが200 (成功) の場合のみ処理を続行
        if (response.getStatusCode() == 200) {
            // JSONレスポンスボディを型付けされていないMapにデシリアライズ
            Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
            // 'animal'キーに対応する値を取得し、Mapにキャスト
            Map<String, Object> animal = (Map<String, Object>) results.get('animal');
            // 'name'キーの値を取得し、デバッグログに出力
            System.debug('取得した動物名: ' + animal.get('name'));
            // 動物名を文字列として返す
            return (String) animal.get('name');
        }
        
        // 成功しなかった場合はnullを返す
        return null;
    }
}

テストクラス: AnimalLocatorTest

Apex コールアウトを含むコードをテストする場合、実際に外部 API を呼び出すことはできません。代わりに HttpCalloutMock インターフェースを実装したクラスを使用して、擬似的なレスポンスを返す必要があります。

// @IsTest
// public class AnimalLocatorMock implements HttpCalloutMock {
//    public HttpResponse respond(HttpRequest req) {
//        // Create a fake response
//        HttpResponse res = new HttpResponse();
//        res.setHeader('Content-Type', 'application/json');
//        res.setBody('{"animal":{"id":1,"name":"chicken"}}');
//        res.setStatusCode(200);
//        return res;
//    }
// }

@IsTest
private class AnimalLocatorTest {
    @IsTest
    static void testGetAnimalNameById() {
        // 1. テスト用の擬似レスポンスを定義するMockクラスを設定
        Test.setMock(HttpCalloutMock.class, new AnimalLocatorMock());

        // 2. テスト対象のメソッドを呼び出す
        // この呼び出しは実際のコールアウトではなく、Mockクラスのrespondメソッドを呼び出す
        String animalName = AnimalLocator.getAnimalNameById(1);

        // 3. アサーション(結果の検証)
        // 期待される値 'chicken' と実際の結果が一致するかどうかを確認
        System.assertEquals('chicken', animalName, 'The animal name did not match the expected value.');
    }
}
// Mockクラスの実装
public class AnimalLocatorMock implements HttpCalloutMock {
    // HttpCalloutMockインターフェースのrespondメソッドを実装
    public HttpResponse respond(HttpRequest req) {
        // 期待されるエンドポイントかどうかを検証
        System.assertEquals('https://th-apex-http-callout.herokuapp.com/animals/1', req.getEndpoint());
        System.assertEquals('GET', req.getMethod());

        // 擬似的なレスポンスを作成
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json;charset=UTF-8');
        // JSON形式のボディを設定
        res.setBody('{"animal":{"id":1,"name":"chicken","eats":"insects","says":"cluck"}}');
        // ステータスコードを200 (成功) に設定
        res.setStatusCode(200);
        return res;
    }
}

注意事項

Apex Callouts を実装する際には、Salesforce プラットフォーム特有の制約や考慮事項がいくつかあります。

リモートサイトの設定 (Remote Site Settings)

Apex コードから外部サイトへコールアウトを行うには、まずその宛先 URL を Salesforce に信頼できるサイトとして登録する必要があります。これは「設定」メニューの「セキュリティ」>「リモートサイトの設定」から行います。ここに登録されていないエンドポイントにアクセスしようとすると、System.CalloutException: Unauthorized endpoint というエラーが発生します。これは、悪意のあるコードが任意の外部サイトに情報を送信するのを防ぐためのセキュリティ機能です。

名前付き認証情報 (Named Credentials)

リモートサイトの設定は手軽ですが、現在では名前付き認証情報 (Named Credentials) の使用が強く推奨されています。名前付き認証情報は、エンドポイント URL と認証情報を一つの定義にまとめたものです。コード内ではエンドポイント URL をハードコーディングする代わりに、名前付き認証情報の URL を指定します(例: `callout:My_Named_Credential/some_path`)。
利点:

  • セキュリティ向上: パスワードや API キーなどの認証情報をコードから分離し、安全に保管できます。
  • メンテナンス性向上: エンドポイント URL や認証情報が変更された場合でも、コードを修正することなく、設定画面で名前付き認証情報を更新するだけで対応できます。
  • コールアウトの簡略化: リモートサイトの設定が不要になります。

ガバナ制限 (Governor Limits)

Salesforce はマルチテナント環境であるため、リソースの公平な利用を保証するためにガバナ制限が設けられています。Apex Callouts にも以下のような主要な制限があります。

  • 1トランザクションあたりのコールアウト数: 100回まで
  • コールアウトのタイムアウト: 最小5秒、最大120秒(合計)
これらの制限を超える可能性がある処理(例: 大量のレコードをループで処理し、それぞれでコールアウトを行う)は、非同期 Apex(Queueable, Batch Apex など)を使ってトランザクションを分割する設計が必要です。

エラー処理 (Error Handling)

外部システムとの通信は、ネットワークの問題、相手サーバーのダウン、API仕様の変更など、様々な要因で失敗する可能性があります。そのため、堅牢なエラー処理は不可欠です。

  • ステータスコードの確認: response.getStatusCode() の値を確認し、2xx 系の成功コード以外の場合の処理を必ず記述します。4xx (クライアントエラー) や 5xx (サーバーエラー) の場合には、ログを記録したり、ユーザーに適切なフィードバックを返したりするべきです。
  • try-catch ブロック: コールアウト処理全体を try-catch ブロックで囲み、System.CalloutException などの予期せぬ例外を捕捉できるようにします。


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

Apex Callouts は、Salesforce を外部の世界と繋ぎ、その価値を飛躍的に高めるための強力なツールです。統合エンジニアとして、私は以下のベストプラクティスを常に意識して設計・実装を行っています。

  1. 名前付き認証情報を常に使用する: セキュリティとメンテナンス性の観点から、リモートサイトの設定よりも名前付き認証情報を優先します。
  2. 非同期処理を積極的に活用する: ユーザー操作をブロックせず、ガバナ制限を回避するために、特にトリガーからのコールアウトや時間のかかる処理には @future(callout=true)Queueable Apex を使用します。
  3. 堅牢なエラーハンドリングを実装する: 通信は必ず成功するとは限りません。ステータスコードのチェックと例外処理を徹底し、失敗した場合の代替処理や通知の仕組みを組み込みます。
  4. テストカバレッジ 100% を目指す: HttpCalloutMock を活用し、成功ケースだけでなく、様々なエラーステータスコード(404, 500など)やレスポンスが返ってきた場合も想定したテストを記述します。
  5. 再試行ロジックを検討する: ネットワークの一時的な問題などでコールアウトが失敗した場合に備え、指数バックオフなどのアルゴリズムを用いた再試行ロジックを組み込むことも、システムの信頼性を高める上で有効です。

これらの原則を守ることで、安全で、信頼性が高く、かつメンテナンスしやすい連携ソリューションを構築することができます。Apex Callouts をマスターし、Salesforce を真の統合プラットフォームとして活用していきましょう。

コメント