SpringのRestTemplateだけでSalesforceのカスタムメタデータレコードを更新する
はじめに
Salesforce Platformでは、組織に関するカスタマイズ設定は「メタデータ」として定義・管理されています。多くの場合はGUIを通じて設定画面から手動操作で行いますが、外部から「メタデータAPI」と呼ばれる専用のAPIを使い、コマンドラインやプログラムから操作することもできます。
その場合の手段として一番の候補に上がるのは公式のSalesforce DX(sfdx CLI)ですが、Javaからアクセスする場合は、公式のWSC(Web Services Connector)があります。
一方、JavaのポピュラーなフレームワークであるSpring Bootでアプリを作成する場合、通常RestClient
, WebClient
, RestTemplate
あたりを使用します。
WSCのHTTP通信はJDK標準のHttpURLConnection
をラップした独自のAPIとなっており、タイムアウト、ロギング、リトライなどの共通的な振る舞いを制御したくなった際に、Spring Bootアプリ側とWSCとで異なる設定方法をメンテしていくのは不便です。またWSCはSalesforceのメジャーリリースに伴うAPIバージョンや依存ライブラリの更新など、継続的にメンテナンスは行われていますが、それ以外の改修は積極的ではないような印象があり、将来性にも少し不安があります。
SalesforceのAPIではSOAP(Simple Object Access Protocol)やXMLなどが登場するためハードルが高いように感じますが、単純なユースケースであればWSCに頼らなくてもよいため、 RestTemplate
での実装を紹介します。
前提条件
- Java 21 (Amazon Corretto 21)
- Spring Boot 3.4.3
メタデータAPI
タイトルにある通り、今回は「カスタムメタデータレコード」の操作を行います。
カスタムメタデータレコードですが、その前に、Salesforce Platformには一般的な業務システムにおけるデータには大きく2種類あります。
- メタデータ
- テーブル・オブジェクトに格納されたレコード(ビジネスデータ)
カスタムメタデータレコードについては、レコードと名前はついていますが、取引先レコードなどのようなオブジェクトのようにREST APIで直接更新することはできません。なぜなら、カスタムメタデータは「メタデータ」として扱われるためです。
カスタムメタデータレコードを操作するには、メタデータAPIが必要になります。
Javaでの実装例
本記事ではJavaアプリケーションにおける application.properties
のように、設定名と設定値のペアをもつカスタムメタデータをサンプルとして取り上げます。
カスタムメタデータ型の作成
アプリケーション設定(Setting
)としてカスタムメタデータ型を作成しましょう。API参照名は Setting__mdt
となります。
設定名については標準のカスタムメタデータレコード名 DeveloperName
をそのまま使います。
設定値についてはカスタム項目として作成する必要があり Value
(API参照名は Value__c
)という項目名の文字列型のカスタム項目で作成します。
Salesforce側の設定は以上です。あとはJava(Spring Boot)側で実装しましょう。
Recordの作成
まず、上記のカスタムメタデータレコードを扱うためのJavaクラスを SettingRecord
として作成します。
最終的にデプロイする際にはXMLが必要になるので、このRecord
のメソッドとして、XMLを返却する toMetadataXml
も用意します。
コード上ではRecord
(Java17〜)、テキストブロック(Java15〜)、formatted
メソッド(Java15〜)を使っています。
public record SettingRecord(String id,
String developerName,
String label,
String namespacePrefix,
String value) {
/**
* メタデータXMLとして返す。
*/
public String toMetadataXml() {
return """
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Setting__mdt.%s</fullName>
<label>%s</label>
<values>
<field>Value__c</field>
<value>%s</value>
</values>
</CustomMetadata>
""".formatted(developerName, label, value);
}
}
また、デプロイ結果をレスポンスとして受け取るためのクラスも作っておきます。
デプロイ結果についてはDeployResult
というオブジェクト構造であり、詳細は下記のドキュメントの通りです。
public record DeployResult(@JsonProperty("id") String id,
@JsonProperty("messages") Object messages,
@JsonProperty("retrieveResult") Object retrieveResult,
@JsonProperty("success") Boolean success,
@JsonProperty("checkOnly") Boolean checkOnly,
@JsonProperty("ignoreWarnings") Boolean ignoreWarnings,
@JsonProperty("rollbackOnError") Boolean rollbackOnError,
@JsonProperty("status") String status,
@JsonProperty("numberComponentsDeployed") Integer numberComponentsDeployed,
@JsonProperty("numberComponentsTotal") Integer numberComponentsTotal,
@JsonProperty("numberComponentErrors") Integer numberComponentErrors,
@JsonProperty("numberTestsCompleted") Integer numberTestsCompleted,
@JsonProperty("numberTestsTotal") Integer numberTestsTotal,
@JsonProperty("numberTestErrors") Integer numberTestErrors,
@JsonProperty("errorStatusCode") String errorStatusCode,
@JsonProperty("errorMessage") String errorMessage) {
}
デプロイ要求に対するレスポンスは、上記のDeployResult
とid
を持つJSONになるようで、こちらのRecord
クラスを作っておきます。
public record DeployResponse(@JsonProperty("id") String id, @JsonProperty("deployResult") DeployResult deployResult) {
}
デプロイ要求
Record
の作成は以上です。いよいよデプロイ要求を行うコードを記述しましょう。
流れとしては以下のようになります。
-
package.xml
を作る - 登録するカスタムメタデータレコード用のメタデータXMLを作る
- 1.と2.をzipファイルでまとめる
- 3.とデプロイオプションを含めたリクエストフォーム作成
- 4.でデプロイ要求をリクエストする
private DeployResponse deployRequest(String accessToken, String restUrl, List<SettingRecord> records) throws IOException {
String deployUrl = restUrl + "metadata/deployRequest";
String packageXml = """
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>*</members>
<name>CustomMetadata</name>
</types>
<version>63.0</version>
</Package>
""";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// 1. package.xml
ZipEntry packageXmlEntry = new ZipEntry("package.xml");
zipOutputStream.putNextEntry(packageXmlEntry);
zipOutputStream.write(packageXml.getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
// 2. カスタムメタデータレコード用のメタデータXML
records.forEach(record -> {
try {
ZipEntry metadataEntry = new ZipEntry("customMetadata/Setting__mdt.%s.md".formatted(record.developerName()));
zipOutputStream.putNextEntry(metadataEntry);
zipOutputStream.write(record.toMetadataXml().getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
// 3. zip作成
byte[] zipBytes = byteArrayOutputStream.toByteArray();
// 4. リクエストフォーム作成
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 4.1 リクエスト(デプロイオプション)
HttpHeaders jsonHeaders = new HttpHeaders();
jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonPayload = """
{
"deployOptions" : {
"allowMissingFiles" : false,
"autoUpdatePackage" : false,
"checkOnly" : false,
"ignoreWarnings" : false,
"performRetrieve" : false,
"purgeOnDelete" : false,
"rollbackOnError" : false,
"runTests" : null,
"singlePackage" : true,
"testLevel" : "NoTestRun"
}
}
""";
HttpEntity<byte[]> jsonEntity = new HttpEntity<>(jsonPayload.getBytes(), jsonHeaders);
body.add("json", jsonEntity);
// 4.2 リクエスト(zip)
body.add("file", new ByteArrayResource(zipBytes) {
@Override
public String getFilename() {
return "package.zip";
}
});
HttpHeaders deployHeaders = new HttpHeaders();
deployHeaders.setBearerAuth(accessToken);
deployHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> deployRequest = new HttpEntity<>(body, deployHeaders);
// 5. リクエスト要求
ResponseEntity<DeployResponse> deployResponse = restTemplate.exchange(deployUrl, HttpMethod.POST, deployRequest, DeployResponse.class);
return deployResponse.getBody();
}
リクエストに必要なアクセストークンと、接続先ごとに異なるAPIのエンドポイントは引数で渡しています。
デプロイ結果の確認
メタデータデプロイは非同期処理のため、上記のリクエストから応答が返ってきても、デプロイが終わっているわけではありません。上記で取得したデプロイIDを用いて、デプロイが終わっているかどうかを調べる必要があります。
今回は、1秒おきに問い合わせを行いながら、デプロイ処理が終わるのを待つようにしましょう。ただし何らかの理由で無限ループするのを避けるため、暫定的に15秒でタイムアウトするようにしています。
// デプロイ処理が終わるまで待つ
private DeployResult waitForCompletion(String accessToken, String restUrl, String deploymentId) {
String deployUrl = restUrl + "metadata/deployRequest/" + deploymentId + "?includeDetails=true";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
// タイムアウト(15秒)
final Duration timeout = Duration.ofSeconds(15);
final long startTime = System.nanoTime();
HttpEntity<String> entity = new HttpEntity<>(headers);
while (Duration.ofNanos(System.nanoTime() - startTime).compareTo(timeout) < 0) {
ResponseEntity<DeployResponse> response = restTemplate.exchange(deployUrl, HttpMethod.GET, entity, DeployResponse.class);
DeployResponse responseBody = response.getBody();
if (responseBody == null) {
throw new ApplicationException("error");
}
DeployResult deployResult = responseBody.deployResult();
String deployResultStatus = deployResult.status();
if (deployResultStatus.equals("InProgress") || deployResultStatus.equals("Pending")) {
Thread.sleep(1000);
} else {
return deployResult;
}
}
throw new ApplicationException("metadata deploy timed out.");
}
これらをまとめた実際のデプロイは以下のような処理になります。
public void deploy(String accessToken, String restUrl, List<SettingRecord> records) {
DeployResponse deployResponse = deployRequest(accessToken, restUrl, records);
DeployResult deployResult = waitForCompletion(accessToken, restUrl, deployResponse.id());
if ((deployResult.status().equalsIgnoreCase("Completed") || deployResult.status().equalsIgnoreCase("Succeeded"))
&& deployResult.errorStatusCode() == null
&& deployResult.numberComponentsTotal() == settings.size()) {
// デプロイ成功
} else {
// デプロイエラー
throw new ApplicationException("deploy error: " + deployResult.errorMessage());
}
}
上記をうまく呼び出してあげれば、カスタムメタデータレコードを登録・更新することができます。
おわりに
本記事では、WSCを使わずにJava + Spring BootのRestTemplate
を使ったカスタムメタデータの登録を行う実装について紹介しました。
ChatGPTをたよりにサクッと動かすつもりだったのですが、なかなかうまく動かず、最終的には公式のドキュメントを見ながら実装するに至りました。
やっていることは本番リリース作業と同等のメタデータのデプロイそのものなので、色々な応用は考えられそうですね。ただ、できればもうちょっとお手軽に操作できるようになってほしいです🙂
Discussion