フローのExternal Servicesで非同期コールアウトの壁にぶち当たった開発者の後悔

external services で実際にやらかした判断

当時の私は、Salesforceの外部連携において「Apexを書かずにフローで完結させる」というトレンドに強く惹かれていました。特に、external servicesがリリースされた当初、「これで開発者の負担が減り、管理者がより多くのことをフローで実現できるようになる」という甘い期待を抱いていました。

とあるプロジェクトで、顧客情報がSalesforceで更新された際に、外部のマスターデータシステムにもその変更をリアルタイムに連携するという要件がありました。ごくシンプルなPOSTリクエストで、JSONボディに顧客IDと更新情報を含めるだけです。

「これならExternal Servicesが最適だ!」と、私は喜び勇んでSwaggerスキーマを登録し、レコードトリガーフローからそのExternal Servicesアクションを呼び出す設計を組みました。当時は非常にスマートな設計だと自負していました。Apexコードは一切不要、Declarativeな設定だけで外部連携が実現できる。これぞローコード開発の真骨頂だと。

同期処理の限界とGovernor Limitの壁

開発初期段階では、数件のテストデータで問題なく動作していました。しかし、実際に本番環境に近いデータ量でテストを進めていくと、すぐに問題が浮上しました。

まず、外部APIの応答速度が不安定な場合があり、フローがタイムアウトを起こし始めたのです。同期コールアウトは最大120秒というタイムアウトがありますが、それ以前にユーザー体験を損なうレベルでフローが固まってしまうことがありました。SalesforceのUI上でレコードを更新したユーザーは、フローが完了するまで画面がロックされたような状態になります。

さらに、一括更新処理やデータインポートの際に、複数のレコードが同時に更新されると、それぞれのレコードでフローがトリガーされ、同期トランザクションにおけるコールアウト回数のGovernor Limitに抵触し始めました。そう、**1トランザクションにつき最大10回**というあの厳しい制限です。当時は「一度の操作でそんなに多くのレコードを更新することはないだろう」と楽観視していたのが、今となっては悔やまれます。

非同期処理への切り替えと、結局Apexに戻った後悔

これらの問題に直面し、私の判断は「非同期処理に切り替えるしかない」という結論に至りました。

ここで私は、External Servicesで登録したAPIを、Apexの@futureメソッドやQueueableインターフェースから呼び出すことを試みました。フローから直接@futureを呼び出すことはできませんが、フローからApexのアクションを呼び出すことは可能です。そのため、フローからApexのラッパークラスを呼び出し、そのApex内でExternal Servicesのアクションを実行する、という変則的な構成を検討しました。

しかし、ここで再び壁にぶつかります。External Servicesで生成されるApexクラス(例: MyExternalService.CustomerUpdate)は、内部でコールアウト処理を実行します。このGenerated Apexクラスは、@future(callout=true) のような非同期メソッド内で直接呼び出すと、コールアウトのコンテキストが重複するか、そもそも非同期コンテキストから同期コールアウトを呼び出すような形になり、Salesforceの制約に抵触することが判明しました。厳密には、Wrapperクラスを自作すれば呼べないこともないのですが、それはExternal Servicesの「宣言的な設定だけで使える」という利点を完全に損なうものでした。

結局のところ、私は「Apexなしでできる」という当初の目論見を捨て、素直にHttpRequestを使ってApexで外部コールアウト処理を書き直す羽目になりました。

public class CustomerApiQueueable implements Queueable, Database.AllowsCallouts {
    private String customerId;
    private String customerName; // 他にも更新するフィールド...

    public CustomerApiQueueable(String id, String name) {
        this.customerId = id;
        this.customerName = name;
    }

    public void execute(QueueableContext context) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/v1/customers/' + customerId);
        req.setMethod('PATCH'); // 更新なのでPATCH
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Authorization', 'Bearer ' + getExternalApiAccessToken()); // 認証トークンの取得

        // JSONボディの構築
        Map<String, Object> bodyMap = new Map<String, Object>();
        bodyMap.put('name', customerName);
        // ... 他のフィールド ...
        req.setBody(JSON.serialize(bodyMap));

        Http http = new Http();
        try {
            HttpResponse res = http.send(req);
            if (res.getStatusCode() != 200 && res.getStatusCode() != 204) {
                // エラー処理、ログ記録など
                System.debug('External API call failed: ' + res.getStatusCode() + ' - ' + res.getBody());
                // 例: Platform Eventを発行して通知
            } else {
                System.debug('External API call successful: ' + res.getStatusCode());
            }
        } catch (Exception e) {
            // 例外処理、リトライロジックなど
            System.debug('External API call exception: ' + e.getMessage());
        }
    }

    // アクセストークン取得のヘルパーメソッド(実際にはよりセキュアな方法で取得)
    private static String getExternalApiAccessToken() {
        // 今ならNamed CredentialやExternal Credentialを使うが、当時は直接実装していた
        // ⚠️ 公式ドキュメント確認が必要 - セキュアなCredential管理方法
        return 'your_access_token_here';
    }
}

// FlowからQueueableを呼び出す場合 (Apex Action経由)
// Public static void enqueueCustomerUpdate(String customerId, String customerName) {
//    System.enqueueJob(new CustomerApiQueueable(customerId, customerName));
// }

// 今ならこうは書かない:
// - Named Credential + External Credential を利用して認証情報をセキュアに管理する。
// - HttpRequest直書きではなく、External Servicesで生成されるApexクラスのメソッドを
//   Queueableから呼べるように、Wrapperクラスをきちんと設計するかもしれない。
//   ただし、それもオーバーヘッドが大きく、HttpRequest直書きと比べて優位性があるか、
//   当時としては疑問だった。
// - Platform Event や Change Data Capture (CDC) を活用し、より疎結合な非同期連携を検討する。

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

この経験から、external servicesは万能ではないと痛感しました。

  • シンプルな同期コールアウトで、かつフローでビジネスロジックを完結させたい場合に、その真価を発揮します。
  • しかし、非同期処理が必要な要件複雑なエラーハンドリング頻繁にスキーマが変わるAPI特殊な認証が必要なAPI、または大量のデータ処理が絡む場合は、最初からApexで実装すべきでした。

「Apexなしでできる」という言葉は常に疑ってかかるべきだと学びました。フローは強力ですが、その裏側にあるGovernor Limitsや実行コンテキストの特性を理解せずに使うと、今回のように結局Apexで書き直す羽目になり、二度手間、三度手間になってしまいます。

当時は、フローだけで外部連携ができて「やった!」と思いましたが、非同期要件が後から出てくる可能性や、将来的な拡張性を考慮せずに安易にExternal Services + フローを選択したことが、私の一番の判断ミスでした。

これは当時の自分向けのメモだ。今なら、要件定義の段階で非同期の可能性や処理量を見極め、迷わずApexでデザインするだろう。External Servicesはあくまで、素早くプロトタイプを作成したり、非常に限定的でシンプルな同期連携に留めるべきだと考えている。

コメント