Salesforceでカスタムメタデータを一括登録、一括更新、一括削除する方法

[**Salesforce Advent Calendar 2022**](https://qiita.com/advent-calendar/2022/salesforce)の8日目の記事です。

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

# はじめに
この記事は[**@Aptenodytes9**](https://qiita.com/Aptenodytes9)さんの[**Salesforceでカスタムメタデータ型を大量INSERTする方法**](https://qiita.com/Aptenodytes9/items/ebf059fe35aea153b494)
のオマージュです。
上記手順でハマったところ、改良したところを中心に書きます。

# カスタムメタデータとは
カスタマイズ可能、リリース可能、パッケージ化可能、アップグレード可能なアプリケーションメタデータです。選択リストやリレーションが使えたり、レコードごとリリースできるのも良い。
詳しく[**こちら**](https://help.salesforce.com/s/articleView?id=sf.custommetadatatypes_overview.htm&type=5)

# Custom Metadata Loader
https://github.com/forcedotcom/CustomMetadataLoader

手法も同じく「Custom Metadata Loader」を使います。

Salesforce公式が提供しているのですが、
[**Salesforce CLI**](https://developer.salesforce.com/ja/tools/sfdxcli)でカスタムメタデータが使えるようになったので、
もうメンテナンスは終了しています。

上記サイトにもCLIで利用できるようになったから、
サポートも終了したし、機能保証はしないよとあります。が、
運用担当者が非エンジニアでsfdxコマンドは厳しかったのでこの手法にしました。

# インストール

<details><summary>インストールは元記事の通りでOKです(一応手順)</summary>

1.「Custom Meta Data Loader」の[**Github**](https://github.com/forcedotcom/CustomMetadataLoader)にアクセス
2.「Deploy to Salesforce」ボタンをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/4783f0e2-3149-657a-a71a-242049fd727b.png)
3.「Deploy to:」で環境を選択して「Login to Salesforce」をクリック(他の項目は変更不要です)
<img width="600" alt="Login to Salesforce" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/0e3820e7-f861-1fc2-33fb-81dfd44fc092.png">
4.アクセスを許可
<img width="300" alt="アクセス許可" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/aea13139-f772-d528-1f22-41c95fe1a523.png">
5.「Deploy」をクリック
<img width="600" alt="アクセス許可" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/2578fef8-1677-dfe2-283d-1972e0a04e6c.png">
「Deployment Complete」が表示されたら完了です。
<img width="120" alt="デプロイ完了" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/dbabfdf8-80d6-8afb-dd33-374f38590101.png">
</details>

# インストール後の設定
<details><summary>途中までは元記事の通りでOKです(一応手順)</summary>

1.利用ユーザのプロファイルの「カスタムアプリケーション設定」で「Custom Metadata Loader」の「参照可能」にチェック
<img width="500" alt="カスタムアプリケーション設定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/3e91f2a6-3b64-d72e-fd63-363060d93057.png">
2.同じくプロファイルの「カスタムタブの設定」で「Custom Metadata Loader」を「デフォルトで表示」に変更
※ 「Custom Metadata Migrator」というカスタム設定、カスタムオブジェクトをカスタムメタデータに移行するツールもありますが今回は不要です。
<img width="360" alt="カスタムタブの設定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/9f65815a-11b5-c536-996d-afa8c468081b.png">
</details>
3.「Visualforceページ」の「CustomMetadataRecordUploader」をクリック

4.「プレビュー」をクリック
5.URLのグレー部分を控える
`https://d7f000005d9hsuas-dev-ed--c.vf.force.com`/apex/CustomMetadataRecordUploader
<img width="600" alt="URLドメイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/a7451f77-dcee-b125-c3b2-c011a0afdfe5.png">
6.「リモートサイトの設定」で「新規リモートサイト」をクリックして以下設定して保存
| 項目 | 設定値 |
| :-- | :-- |
| リモートサイト名 | 任意 |
| リモートサイトのURL | <font color="Red">手順5で控えたURL</font> |
| プロトコルセキュリティの無効化 | チェックなし |
| 説明 | 任意 |
| 有効 | チェックあり |

7.アプリケーションの「Custom Metadata Loader」をクリックして画面が表示されたら準備完了
<img width="550" alt="URLドメイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/3446fda0-87c4-771a-cfc1-3d70a1e9a95b.png">

# CSVファイルの作成
以下フォーマットで作成
| 項目 | 設定値 |
| :-- | :-- |
| 文字コード | UTF-8 |
| 改行コード | CRLF |
| BOM | なし  <br><font color="Red">※ExcelでCSV保存した場合はBOMがつくので注意</font> |
| データ構成 | 1行目はヘッダー<br>2行目以降がデータ |
| シングルクォーテーション<br>ダブルクォーテーション | なし |
| ヘッダー | API参照名 |
| 必須の列 | 表示ラベル(Label)<br>カスタムメタデータレコード名(DeveloperName)<br><font color="Red">※「表示ラベル」のAPI参照名は「MasterLabel」だがインポート時に設定するのは「Label」<br><font color="Red">※Descriptionはなくても問題なし</font>  |

# インポート
<img width="650" alt="テスト用カスタムメタデータ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/dbe9e834-5dc7-a965-76e2-843ae779201b.png">

上記のようなカスタムメタデータ型に対して以下のようなCSVを用意します。
(インストール時にテスト用の「CountryMapping」というカスタムメタデータが生成されます)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/db3ad06d-6268-44e1-0afc-d0568acf9557.png)

1.「ファイルを選択」でCSVを選択し、
右上の選択リストから対象のカスタムメタデータ型を選択して「Create/Update custom metadata」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/bfb32d28-d502-7059-89bf-2236edc7ad75.png)
<font color="Red">※正常終了すると登録済みのカスタムメタデータが表示されます(なぜか今回登録した分は表示されません)。</font>
<font color="Red">※カスタムメタデータレコード名(DeveloperName)が存在する場合は更新します。</font>


正常に登録されました。
<img width="550" alt="登録完了カスタムメタデータ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/56cbf4e3-84b6-4112-2c3c-f896afbc71ee.png">

ここから改良点になります。

# 改良1 空値を登録できるようにする
実は現状のままでは**空値**を登録することができません。
そこで以下のファイルを修正します。
[split(regExp, limit)](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_string.htm#apex_System_String_split_2)メソッドのlimitに負の数を設定することで空文字が破棄されなくなります。

```diff_java:CustomMetadataLoader/custom_md_loader/classes/CSVFileUtil.cls
- List<String> fields = line.split(',');
+ List<String> fields = line.split(',', -1);
```

# 改良2 カスタムメタデータを一括で削除できるようにする
現状のままだと1レコードずつ画面からしか削除できないので、
選択したカスタムメタデータ型のレコードをすべて削除するようにします。

```diff_java:CustomMetadataLoader/custom_md_loader/classes/CustomMetadataUploadController.cls
+ public PageReference deleteCustomMetaData() {
+     ApexPages.getMessages().clear();
+     showRecordsTable = false;
+     deleteCustomMetadtaRecords();
+     // TODO: Add Success Message / Display Errors
+     return null;
+ }
+
+ private void deleteCustomMetadtaRecords() {
+     if(selectedType == 'Select type') {
+         ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, 'Please choose a valid custom metadata type.');
+         ApexPages.addMessage(errorMessage);
+         return;
+     }
+
+     String query = 'SELECT DeveloperName FROM ' + selectedType;
+
+     List<sObject> customRecords = Database.query(query);
+     List<String> recordsToDelete = new List<String>();
+
+     for (sObject record : customRecords) {
+         recordsToDelete.add(selectedType + '.' + record.get('DeveloperName'));
+     }
+
+     MetadataUtil.deleteMetadata('CustomMetadata', recordsToDelete);
+ }
```

```diff_java:CustomMetadataLoader/custom_md_loader/classes/MetadataUtil.cls
+ public static void deleteMetadata(String type_x,String[] fullNames) {
+     List<MetadataService.DeleteResult> results = getPort().deleteMetadata(type_x, fullNames);
+     if (results!=null) {
+         for (MetadataService.DeleteResult deleteResult : results) {
+             if (deleteResult==null || deleteResult.success) {
+                  continue;
+             }
+             // Construct error message and throw an exception
+             if (deleteResult.errors!=null) {
+                 List<String> messages = new List<String>();
+                 messages.add((deleteResult.errors.size()==1 ? 'Error ' : 'Errors ') + 'occured processing component ' + deleteResult.fullName + '.');
+                 for(MetadataService.Error error : deleteResult.errors){
+                     messages.add(error.message + ' (' + error.statusCode + ').' + ( error.fields!=null && error.fields.size()>0 ? ' Fields ' + String.join(error.fields, ',') + '.' : '' ) );
+                 }
+                 if(messages.size()>0){
+                     ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, String.join(messages, ' ')));
+                     System.debug(LoggingLevel.ERROR, String.join(messages, ''));
+                     return;
+                 }
+             }
+             if(!deleteResult.success){
+                 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, 'Request failed with no specified error.'));
+                 return;
+             }
+         }
+     }
+ }
```

```diff_html:CustomMetadataLoader/custom_md_loader/pages/CustomMetadataRecordUploader.page
+ <div align="center" draggable="false" >
+    <apex:ActionRegion >
+     <apex:commandButton value="Delete Custom MetaData Type" action="{!deleteCustomMetaData}" rendered="true"/>
+     <apex:actionSupport event="onclick"  action="{!deleteCustomMetaData}" rendered="{!showRecordsTable}" oncomplete="window.opener.location.reload();"/>
+   </apex:ActionRegion>
+ </div>
+ <br/>
+ <br/>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39536/5c6a3f9f-f9d0-82d7-15c3-70c00c6520cc.png)
これでカスタムメタデータ型を選択して「Delete Custom MetaData Type」をクリックするとレコードを全部消します。

まとめたものは以下

https://github.com/necoyama3/CustomMetadataLoader