AppExchangeアプリにおけるカスタムメタデータ型の利用について

co-meetingの遠藤です。Salesforce Platform Advent Calendar 2018 12日の記事になります。

AppExchange向けのパッケージで、インストールされた組織で作成されたカスタムオブジェクトなど任意のオブジェクトに対応する機能の提供を検討した際に、弊社co-meetingでカスタムメタデータ型について調べた内容を改めてまとめてみました。

AppExchange向けパッケージ、つまり、管理パッケージに含まれるカスタムメタデータ型とそのレコードが登録者組織 (※) でどのように振る舞うのか、また、登録者組織のユーザにカスタムメタデータ型レコードの追加・変更を許可する場合に、レコードを管理するパッケージ独自の画面をApex CustomMetadataクラスの使って実装可能かどうか、といったあたりを中心に解説しています。

本記事ではカスタムメタデータ型を取り上げていますが、どちらかというとAppExchangeアプリにSalesforceの機能を採用するかどうかを検討する際の一例として、これからAppExchangeのアプリを開発される方々の参考になればと思います。

※ 登録者組織 Subscriber Orgはパッケージをインストールした組織のことです。

## カスタムメタデータについておさらい

カスタムメタデータ型とは何かについては、以下のTrailheadモジュールが参照になります。

- [カスタムメタデータ型](https://trailhead.salesforce.com/ja/content/learn/modules/custom_metadata_types)
- [再利用可能な Lightning コンポーネントの作成](https://trailhead.salesforce.com/ja/content/learn/projects/build-reusable-lightning-components)

カスタムオブジェクトやカスタム設定と異なるポイントをいくつか挙げてみます。

1. カスタムメタデータ型、そのカスタムフィールドはパッケージからの削除が不可、レコードは削除可能
2. レコードを配布可能
3. [設定]にカスタムメタデータ型のレコードを管理する画面が用意されている
4. レコードの参照はSOQLで取得、参照回数のガバナ制限がない ([カスタムメタデータの割り当ておよび使用量の計算](https://help.salesforce.com/articleView?id=custommetadatatypes_limits.htm&type=5))
5. レコードはキャッシュされパフォーマンス的に有利 ([カスタムメタデータ型の制限事項 キャシュ](https://help.salesforce.com/articleView?id=custommetadatatypes_limitations.htm&type=5)
6. レコードの更新・削除はシステム管理者のみが可能 (画面上でのみ確認、一次ソース未確認)
7. Apex作成・更新が可能だが削除はできない (SOAPのメタデータAPIであれば可能?)
8. カスタムメタデータ型、そのレコード、およびカスタムフィールドのレベルで保護可能
9. カスタムメタデータリレーション型というオブジェクトやオブジェクトのフィールドを設定できるデータ型が便利

3, 4, 5, 6, 8の一部はカスタム設定でも同様です。

## コンポーネントの削除について確認

管理パッケージで新しい機能の利用を検討する場合、まずは[管理パッケージで使用可能なコンポーネント](https://help.salesforce.com/articleView?id=packaging_packageable_components.htm)で削除可能なコンポーネントかどうかを確認しておきたいところです。

管理パッケージのコンポーネントは、カスタムオブジェクトやApexクラスをはじめ多くのコンポーネントは削除可能になりましたが、特に新しめの機能は削除できないものが多いような気がします。
削除可能性に加えて、カスタムメタデータ型についてパッケージ開発者側で特に気にする項目を含めて抜き出してみました。

| コンポーネント     | アップグレード可能 | 開発者による削除可能 | 保護可能 |
|:-----------------|:---------------:|:----------------:|:--------:|
| カスタムメタデータ型 | Yes            | No               | Yes     |
| カスタムメタデータレコード | Yes       | Yes              | Yes     |
| カスタムメタデータ型のカスタム項目 | Yes | No               | No ※   |

カスタムフィールドは削除できないため、仕様が固まっていない場合は使用をためらわれそうです。
また、カスタムフィールドの保護については「No」となっていますが、フィールド設定の[項目管理可能性]の選択によって振る舞いが変わります。こちらは後述します。

TIPS:「管理パッケージで使用可能なコンポーネント」に記載されている削除可能なコンポーネントは、[パッケージ化組織でのコンポーネントの削除](https://developer.salesforce.com/docs/atlas.ja-jp.216.0.packagingGuide.meta/packagingGuide/packaging_managed_component_deletion.htm)をケース申請して有効化する必要があります。パッケージ開発組織で管理されたカスタムオブジェクトに削除ボタンがない場合は、ケース申請してみましょう。

## カスタムメタデータ型の保護の振る舞いの確認

次に、カスタムメタデータ型に関連するコンポーネントの保護について確認してみます。

以下の3つレベルで保護可能です。

- カスタムメタデータ型
- カスタムメタデータ型のレコード
- カスタムメタデータ型のフィールド

参照: [カスタムメタデータ型およびレコードの保護](https://trailhead.salesforce.com/ja/content/learn/modules/custom_metadata_types/custom_metadata_types_manageability)

以下、それぞれ、ポイントを絞って確認してみます。

カスタムメタデータ型のレコードとカスタムフィールドの保護の確認では、カスタムメタデータ型の表示設定は「公開」つまり保護なしのに設定して確認します。
レコードとカスタムフィールドの保護設定は、カスタムメタデータ型の[表示]は「公開」つまり保護なしの場合に有効な設定であるためです。

### カスタムメタデータ型の保護

カスタムメタデータ型そのものの保護については、カスタム設定とほぼ同じです。
[表示]設定を保護にすると、登録者組織ではカスタムメタデータ型の存在自体が隠蔽されます。

注意すべき点としては、公開型でリリース後は保護には戻せないことくらいでしょうか。

<img width="653" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/3977cd11-14fd-01db-bf7f-e3411380e2f7.png">

参照: [カスタムメタデータ型パッケージ関する考慮事項](https://help.salesforce.com/articleView?id=custommetadatatypes_req_managed_pkgs.htm&type=5)

### カスタムメタデータ型レコードの保護

管理パッケージにカスタムメタデータ型レコードを含めたいユースケースの一つとして考えられるのは、標準オブジェクトに対する設定を含むレコードをパッケージとして提供しておき、カスタムオブジェクトなど登録者組織で定義されたオブジェクトなどの設定はパッケージを利用する側でも追加可能にしたいといったものです。

このように、管理パッケージに含めたレコードは登録者組織のユーザに変更してほしくない場合、レコードの保護機能を利用できます。

確認用のサンプルとしては、[再利用可能な Lightning コンポーネントの作成](https://trailhead.salesforce.com/ja/content/learn/projects/build-reusable-lightning-components)の「Indicator Badge」を利用しています。
カスタムメタデータ型「Indicator Badge」に以下のレコードを追加して「商談」のレコードのみ保護してみます。

| Indicator Badge名 | sObject     | Related Field | 保護コンポーネント |
|:-----------------|:---------------:|:----------------:|:--------:|
| Opportunity | 商談        | 金額    | On  |
| Account     | 取引先       | 種別   | Off  |

保護コンポーネントのチェックボックスは、レコードの編集で設定可能です。
<img width="780" alt="カスタムメタデータ型___Salesforce.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/28827/8ca089af-0584-5b4d-13d2-6dfc9b40b088.jpeg">

以下の画像は登録者組織でカスタムメタデータ型 Indicator Badgeのレコードの管理です。

<img width="984" alt="スクリーンショット 2018-04-11 17.08.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/1301fc72-f41b-d5fb-24c7-fad9f1e93583.png">

「取引先責任者」は登録者組織で追加したレコードです。「取引先」のみ参照可能で、「商談」の存在はわかりません。

当然ですが、SOQLでも取得はできません。

<img width="994" alt="image (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/2c4abaef-36a8-8acb-ef56-f2bf16ed7949.png">

ちなみに、保護コンポーネントは、保護をOnからOffに切り替えは可能ですが、保護していないレコードを保護に変更することはできません。実際に設定してみると以下のメッセージが表示されます。

<img width="791" alt="スクリーンショット 2018-12-12 22.39.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/81823edd-d24d-674f-951a-37f22292dc60.png">

### カスタムメタデータ型カスタムフィールドの保護

カスタムメタデータ型のフィールドの保護は、カスタムメタデータ型自体のフィールドを変更できるかどうかではなく、カスタムメタデータ型レコードのフィールド値を登録者組織で変更可能かどうかの設定になります。

こちらは、サンプルは[カスタムメタデータ型の作成および管理](https://trailhead.salesforce.com/ja/content/learn/modules/custom_metadata_types/custom_metadata_types_create)の「Threat Tier」で確認してみます。

以下のように「Account Tier」を「アップグレード可能(保護)」に、「Vacation」を「登録者が編集可能」に設定して管理パッケージに含めます。

<img width="625" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/1b0ac665-fb91-e7a6-4e22-261f0645a35c.png">

登録者組織でパッケージに追加したレコードを参照してみると、「Account Tier」フィールドは変更不可になり、「Vacation」は変更可能になっています。

<img width="724" alt="スクリーンショット 2018-12-12 14.59.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/ca7b596c-12d1-aec8-8e58-f87192d07b81.png">

## 設定画面の検討

公開型のカスタムメタデータ型の場合登録者組織では以下のように一覧に表示されレコードの管理が可能です。
<img width="789" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/88147a6f-d673-1459-3bf5-2a633ea372ab.png">

レコードの新規作成画面
<img width="688" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/88c422b5-8cc6-f4ed-24a5-d120c60a05a5.png">

デフォルトの設定画面で可能な設定ですが、メタデータリレーション型はオブジェクトの参照やフィールドをプルダウンで選択可能であったり、カスタムメタデータ型のフィールドにも入力規則も設定可能なので、手順書を整えればAppExchangeのアプリケーションでも使えそうです。

とはいえ、必要最低限の画面となるため設定が複雑になる場合は、登録者組織のシステム管理者自らが導入を行うようなパッケージでは機能不足な場合もありそうです。

そこで、カスタムメタデータ型の設定画面として独自の実装をすることが可能かどうかという観点で、Apexの[CustomMetadataクラス](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_CustomMetadata.htm)を利用したレコードの管理について調べてみました。

### カスタムメタデータ型レコードをApexで作成

ミニマムにデフォルト設定画面で機能を提供しつつ、あとから設定画面を独自に用意したい場合、Apexメタデータ APIが利用できます。
インストールの際にSOAPのエンドポイントを許可する必要がないなどメリットがありますが、SOAPのメタデータAPIの呼び出しと同様に適用は非同期処理なため、オンライン処理ではハンドリングが難しいのが難点です。

Apexメタデータ APIは、名前空間[Metadata](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_namespace_Metadata.htm)で提供されるApexクラスです。
メタデータ型レコードの管理で使用する主なクラスは以下のようなものがあります。

* [CustomMetadata クラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_class_Metadata_CustomMetadata.htm#apex_class_Metadata_CustomMetadata): カスタムメタデータ型のレコードを表すクラス
* [CustomMetadataValue クラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_class_Metadata_CustomMetadataValue.htm#apex_class_Metadata_CustomMetadataValue): カスタムメタデータ型のレコードのフィールドと値を表すクラス
* [DeployContainer クラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_class_Metadata_DeployContainer.htm#apex_class_Metadata_DeployContainer): メタデータの追加・削除を管理するコンテナクラス。
* [Operations クラス](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_Operations.htm#apex_class_Metadata_Operations) メタデータの取得、デプロイを提供するクラス。

コーディングの流れについては、[Operations クラス](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_Operations.htm#apex_class_Metadata_Operations)のExample: Deploy Metadataが参考になります。

Trailheadは、モジュール[Apex メタデータ API](https://trailhead.salesforce.com/ja/content/learn/modules/apex_metadata_api)の[自動設定変更のためのシステム管理者ツールの作成](https://trailhead.salesforce.com/ja/content/learn/modules/apex_metadata_api/apex_metadata_api_tools)にも手順が解説されていました。

以下、Apex メタデータ APIの使い方について簡単にまとめてみます。

メタデータのデプロイは非同期に実行されるため`DeployCallback`インタフェースを実装するコールバッククラスを定義します。

```
public class MyCallback implements Metadata.DeployCallback {
    public void handleResult(Metadata.DeployResult result,
                             Metadata.DeployCallbackContext context) {
        if (result.status == Metadata.DeployStatus.Succeeded) {
            System.debug(result);
        } else {
            System.debug(result);
        }
    }
}
```

`handleResult`メソッドには、`DeployResult`クラスと`DeployCallbackContext`クラスのインスタンスが渡ってきますが、入力規則違反などエラーが発生した場合は、`DeployResult`クラスのインスタンスここでは`result`のプロパティを、`result.details.componentFailures[n].problem`のように辿るとエラーの内容を取得できます。

例えば、メタデータリレーション型 項目定義制御のフィールドに、オブジェクトに無いフィールドをセットした場合以下のようなエラーが含まれます。

```
Related Field: 制限つき選択リスト項目の値が不適切: Lead.StatusXXXX
```

また、`DeployCallbackContext`のインスタンスからは、`getCallbackJobId`メソッドで、非同期処理のジョブIdを取得できます。

メタデータ型レコードの追加は以下のような流れで実装します。

```
public function save() {
	Metadata.DeployContainer mdContainer = new Metadata.DeployContainer();

	Metadata.CustomMetadata customMetadata =  new Metadata.CustomMetadata();
	customMetadata.label='VAT_Data.Ch';
	customMetadata.fullName = 'VAT_Data.Ch';
	Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue();
	customField.field = 'Country_Name__c';
	customField.value = 'CH';
	customMetadata.values.add(customField);
	Metadata.CustomMetadataValue customField2 = new Metadata.CustomMetadataValue();
	customField2.field = 'VAT_Rate__c';
	customField2.value = '80';
	customMetadata.values.add(customField2);
	mdContainer.addMetadata(customMetadata);

	Metadata.Operations.enqueueDeployment(mdContainer, new MyCallback());
}

```

`Metadata.CustomMetadata`クラスのインスタンスが、レコードに対応しています。
`CustomMetadataValue`クラスはカスタムフィールドに対応します。

`Operations.enqueueDeployment`メソッドに、`DeployContainer`のインスタンスとコールバックを渡してメタデータのデプロイを実行します。

更新については、`fullName`が合致するレコードがスレにあればそれを更新します。

### Apex メタデータ APIの制限事項

#### レコードの削除ができない

DeployContainer クラスには、メタデータ削除用と思われるメソッド[removeMetadata](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_DeployContainer.htm#apex_Metadata_DeployContainer_methods)、[removeMetadataByFullName](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_DeployContainer.htm#apex_Metadata_DeployContainer_methods)がありますが、いずれも実行してみると「空のコンテナはリリースできません」というエラーが返ります。

#### コールバック以外に追跡方法が無い

`Operations.enqueueDeployment`メソッドが返すデプロイメントリクエストのジョブIdの使い方が、不明です。

[DeployCallbackContext.getCallbackJobId](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Metadata_DeployCallbackContext.htm#apex_class_Metadata_DeployCallbackContext)の説明には「Gets the asynchronous Apex job ID for the callback job.」とありますが、非同期Apexジョブが実行された形跡が無いです。

継続して調査したいところですが、実行時のジョブIdとコールバックのジョブIdを独自に管理すればユーザに結果をフィードバックする機能は実装できそうです。

### Apexを利用した独自画面の利用まとめ

上記の通り、Apexからレコードを作成することはできますが、画面を用意したオンライン処理で実装するのは若干工夫が必要そうです。
[Apex メタデータ API の使用開始](https://trailhead.salesforce.com/ja/content/learn/modules/apex_metadata_api/apex_metadata_api_intro)の[ISV のためのメモ]にも、ユースケースとしては本記事と同じように「システム管理者にプロフェッショナルサービスの専門知識がなくても使用できるツールを提供し、すぐに設定を変更できるようにする。」ことと書いてありますが、独自UIの実装自体はカスタムオブジェクトの場合とくらべてコストが高くなってしまうため、カスタムメタデータ型の他のメリットと比較して適用を検討する必要があります。

## その他 カスタムメタデータ型についてあれこれ

今回新たに確認してみたものをいくつか

### カスタムメタデータ型カスタムフィールドの入力規則

カスタムオブジェクトと同様にフィールドに入力規則を設定できます。

メタデータリレーション型 項目定義制御のフィールドの型を指定したい場合は、以下のように指定できました。

<img width="665" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/28827/c6ba1c86-6f03-5421-3087-cbe0c61c30b1.png">

エラー条件数式を以下のように設定して、データ型が選択リストのフィールドのみを設定可能にしています。

```
UPPER(Accessories_Field__r.DataType) <> 'PICKLIST'
```

### メタデータ リレーション型のユースケースは？

[メタデータリレーション項目の使用](https://trailhead.salesforce.com/ja/content/learn/modules/custom_metadata_types/custom_metadata_types_create_md_relationships)で説明されているようにカスタムメタデータ型は、他のカスタムメタデータ型レコードへの参照フィールドも追加できます。

ただ、この例では、イマイチ使い所が不明です。
あるとすると親子のレコードをセットで配布したい場合くらいしか思いつきませんでした。
いいユースケースがあれば教えて下さい。

## まとめ

一通り調べた内容をまとめ直してみました。

管理パッケージでカスタムメタデータ型を適用をすぐに検討できそうなケースは以下のような例がありそうです。

- 登録者組織でシステム管理者がレコードを変更・追加する必要がない。つまり、パッケージが提供するカスタムメタデータ型およびレコードをすべて保護可能なケース。例: 日本の祝日設定など配布したい。
- パッケージでリスト型のカスタム設定を使用していて、カスタムメタデータ型への移行が必要。Spring 18よりリスト型のカスタム設定はデフォルト無効となっている。参考: [Access Schema Settings in Setup | Spring 18 リリースノート](http://releasenotes.docs.salesforce.com/en-us/spring18/release-notes/rn_forcecom_general_schema_settings.htm)
- [再利用可能な Lightning コンポーネントの作成](https://trailhead.salesforce.com/ja/content/learn/projects/build-reusable-lightning-components)の例のように、レコードページに配置するコンポーネントの設定など設定項目数が小さい場合

このあたりが、パッケージで筋よくカスタムメタデータ型を利用可能なケースと考えられそうです。
上記に当てはまらない場合は、従来どおりカスタムオブジェクトで実装する場合と提供する機能の性質を考慮して比較検討する必要があります。

ちなみに、この調査をする目的の任意のオブジェクトに対応した機能を提供する設定は、カスタムオブジェクトでも実装は可能です。しかし、カスタムメタデータ型では省略可能な以下のような実装が必要になります。

- パッケージとしてデフォルト設定レコードを入れたい場合はPostInstallHandlerで追加する必要がある。
- 権限周りの設計(レコード保護機能に替わる仕組みを独自に実装する必要がある)。
- 設定画面を用意する場合、メタデータリレーション型に替わるようなオブジェクト設定、カスタムフィールド設定などのUIも実装する必要がある。
- レコードを利用する側の振る舞いはあまりカスタムメタデータ型とあまり変わらない。ガバナ制限のSOQL発行カウントは消費する。

とカスタムオブジェクトで実装するのもそれなりにコストが掛かるので、判断は難しいところです。