Salesforceで外部サービスと非同期に連携するアーキテクチャの考察

![SalesforceAdventCalendar2024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33258/49735f3c-4e12-31b9-0820-f341666f212a.png)

Salesforce界隈の皆さま、今年もお疲れさまでした！
1年の締めくくりといえば、やっぱり技術系アドベントカレンダー。新しい知識を得る喜びや、課題を乗り越える楽しさを共有し合いましょう。

## はじめに

Salesforceにおいて、外部サービスと<strong>非同期</strong>に連携する汎用的なコンポーネントを開発するために考えたことを書いていきます。
前提として汎用的なコンポーネントを画面に配置して外部サービスを非同期で呼び出すようにするためにLightning Web Comoponentsを利用します。

Salesforceから外部サービスを非同期に呼び出す場合には下記のようなハードルがあります。

- 外部サービスにリクエストを送る際に、固有のコリレーションIDを指定できるか？
- コールバック処理でコリレーションIDから対象オブジェクトを特定できるか？
- コールバック処理で任意の処理を実行できるか？

これらのハードルを解消する方法についてアーキテクチャを検討していきます。

## Salesforceの処理から外部サービスに戻り先を渡す必要がある

まず、ポイントとなることとしては「非同期」である点です。
同期的なコールアウトであれば、Httpクラスを利用して外部サービスのWebAPIをコールするだけなので簡単です。
しかし、非同期であるために戻り先を指定する必要があります。

具体的には、外部サービス側の処理が終わったときにコールバックするためのCallback URLとコールバック処理内でデータを書き戻すなどの処理を実行するときにデータを特定す仕組みが必要となります。

ここからはそれらに必要な仕組みについて検討します。

### コリレーションIDの生成と利用

Salesforce側のデータと外部サービスのコールバックを関連付けるためには、外部サービスにリクエストを送る際に、固有のコリレーションID（SalesforceのレコードIDやカスタムユニークID）を渡すことが一般的です。
外部サービスはこのコリレーションIDをそのままコールバック時に返すことで、呼び出し元を特定できます。

```java
public static void sendRequestToExternalService(String recordId) {
    // 外部サービスへのリクエストを準備
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint('https://external-service.example.com/api');
    request.setMethod('POST');

    // コリレーションIDを渡す
    String correlationId = recordId; // 例: SalesforceレコードIDを利用
    request.setBody('{"correlationId": "' + correlationId + '", "callbackUrl": "https://yourInstance.salesforce.com/services/apexrest/callback"}');
    request.setHeader('Content-Type', 'application/json');

    HttpResponse response = http.send(request);

    // 応答を処理
    System.debug('Response: ' + response.getBody());
}
```

### Apex RESTエンドポイントの活用

SalesforceでコールバックするためREST APIのエンドポイントを用意する必要があります。そのためにApex RESTエンドポイントを活用します。
Apex RESTエンドポイントを外部サービスから認証なしでURLをコールしようとした場合、サイト機能の利用、など管理パッケージに含めることができない機能を利用する必要があります。
そこで、Apex RESTエンドポイントを呼び出すときには何らかの方法で認証します。
そのため、外部サービスでCallback URLと認証情報を事前に設定します。

以下は、Apex RESTエンドポイントを定義するサンプルコードです。

```java
@RestResource(urlMapping='/callback')
global with sharing class CallbackHandler {
    @HttpPost
    global static void handleCallback() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // 外部サービスからのデータを取得
        String externalId = req.params.get('externalId');
        String status = req.params.get('status');
        
        // 必要な処理を実行
        if (status == 'success') {
            System.debug('External service completed successfully for ID: ' + externalId);
        } else {
            System.debug('External service failed for ID: ' + externalId);
        }
    }
}
```

### Lightningレコードページの活用

次に、考えるポイントとしてはコールバック処理でデータを特定するためのキーであるレコードIDを汎用的な仕組みで取得する方法です。

Lightning Web Components 間でのデータのやり取りとしては、publicプロパティ・publicメソッド、カスタムイベント、Lightning Messeging Serviceが考えられます。しかし、プロコードによるインテグレーションはできるだけ避ける方針とするため、これらの実装は採用できません。

そこで、LightningレコードページによってレコードIDを渡す方式を採用します。

LightningレコードページによってレコードIDを渡すサンプルコードです。

```javascript:JavaScript ファイル (myRecordComponent.js)
import { LightningElement, api } from 'lwc';

export default class MyRecordComponent extends LightningElement {
    @api recordId; // レコードIDを受け取るプロパティ

    connectedCallback() {
        console.log('Received recordId:', this.recordId);
        // ここで recordId を使った処理を行うことができます。
    }
}
```

```html:HTML ファイル (myRecordComponent.html)
<template>
    <lightning-card title="Record Details" icon-name="standard:record">
        <div class="slds-m-around_medium">
            <p>Record ID: {recordId}</p>
        </div>
    </lightning-card>
</template>
```

```xml:メタデータ ファイル (myRecordComponent.js-meta.xml)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="myRecordComponent">
    <apiVersion>62.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target> <!-- レコードページに配置可能 -->
    </targets>
</LightningComponentBundle>
```

## 外部サービスからコールバックされたときに呼び出し元を特定する

外部サービスからコールバック処理を呼び出したとしても、汎用的な仕組みであるためコリレーションIDだけでは処理を実行することができません。
そのため、コールバックされたデータを使って柔軟な処理を行うには、外部サービスから戻されたコリレーションIDとしてのレコードIDから対象オブジェクトを特定する必要があります。

### レコードIDから対象オブジェクトを特定する

対象オブジェクトが特定でき、メタデータ情報が取得できると、対象オブジェクトに対してデータ更新することができます。

レコードIDから対象オブジェクトを特定するためのサンプルコードです。

```java
public static void describeObjectById(Id recordId) {
    // IDの接頭辞を取得
    String prefix = String.valueOf(recordId).substring(0, 3);

    // 全オブジェクトのDescribe情報を取得
    Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
    Schema.SObjectType objectType = null;

    // 接頭辞に基づき、対象オブジェクトを検索
    for (Schema.SObjectType sObjectType : globalDescribe.values()) {
        if (sObjectType.getDescribe().getKeyPrefix() == prefix) {
            objectType = sObjectType;
            break;
        }
    }

    if (objectType != null) {
        // オブジェクトのメタデータ情報を取得
        Schema.DescribeSObjectResult objectDescribe = objectType.getDescribe();
        System.debug('Object Label: ' + objectDescribe.getLabel());
        System.debug('Object Name: ' + objectDescribe.getName());
    } else {
        System.debug('Object not found for prefix: ' + prefix);
    }
}
```

## コールバック時に任意の処理を実行できるようにする

コールバックされたデータを使って柔軟な処理を行うには、以下のような方法があります。

### Platform Eventを利用した処理の柔軟化
Platform Eventを使うことで、Salesforce内での処理を非同期かつ疎結合で実行できます。
Platform Eventを利用することで、フローやApexを利用して処理を拡張することができます。

```java:Platform Eventの定義例
// 定義されたプラットフォームイベント
event MyCallbackEvent__e {
    String CorrelationId__c;
    String Status__c;
}
```

```java:コールバックハンドラーでイベントを発行
@RestResource(urlMapping='/callback')
global with sharing class CallbackHandler {
    @HttpPost
    global static void handleCallback() {
        RestRequest req = RestContext.request;

        // Platform Eventの発行
        MyCallbackEvent__e event = new MyCallbackEvent__e();
        event.CorrelationId__c = req.params.get('externalId');
        event.Status__c = req.params.get('status');
        EventBus.publish(event);
    }
}
```

### Apexクラスの動的呼び出しによる柔軟化

Apexクラスのクラス名をカスタムメタデータ型に定義し、それを動的に呼び出すことでコールバック処理を柔軟に拡張することができます。

以下のコードで、カスタムメタデータ型からクラス名を取得し、Typeクラスを使用してインスタンス化およびメソッドの実行を行います。

```java:動的呼び出しコード
public class DynamicClassExecutor {
    public static void executeClass(String label) {
        // カスタムメタデータ型からクラス名を取得
        ClassMapping__mdt metadata = [
            SELECT ClassName__c 
            FROM ClassMapping__mdt 
            WHERE DeveloperName = :label
            LIMIT 1
        ];
        
        if (metadata == null || String.isEmpty(metadata.ClassName__c)) {
            throw new IllegalArgumentException('No class name defined for label: ' + label);
        }

        // クラス名をTypeクラスで動的にロード
        Type classType = Type.forName(metadata.ClassName__c);
        if (classType == null) {
            throw new IllegalArgumentException('Class not found: ' + metadata.ClassName__c);
        }

        // インスタンス化とメソッド実行
        Object instance = classType.newInstance();
        if (instance instanceof DynamicClassInterface) {
            ((DynamicClassInterface)instance).execute();
        } else {
            throw new IllegalArgumentException('Class does not implement DynamicClassInterface: ' + metadata.ClassName__c);
        }
    }
}
```

```java:インターフェースの定義
public interface DynamicClassInterface {
    void execute();
}
```

```java:サンプルクラスの実装
public class MyDynamicClass implements DynamicClassInterface {
    public void execute() {
        System.debug('MyDynamicClass is executed!');
    }
}
```

## まとめ

Salesforceで外部サービスと非同期に連携する際に考慮すべきポイントを整理しました。

1. コリレーションIDを渡すことで呼び出し元を特定
2. コリレーションIDから対象オブジェクトを特定
3. Platform EventやApexクラスを活用して柔軟なコールバック処理を実現

これらを適切に組み合わせることで、Salesforceと外部サービス間の汎用的な非同期連携を構築できます。

最後になりますが、この投稿は [Salesforce Adevent Calendar 2024](https://qiita.com/advent-calendar/2024/salesforce) の第8日目の投稿となります。

https://qiita.com/advent-calendar/2024/salesforce

この記事は、 note.com に投稿した記事の転載になります。

https://note.com/a_kuratani/n/nc0dbdee18ebb