Salesforceのコンテンツドキュメントを、Apexクラスで操作してみた

はじめに

Salesforceを利用していて「ファイルを保存する」というのはよくあるシチュエーションかと思います。
ただ、このファイルについて、

・画面フロー、開発画面からファイルをアップロードしたファイルを、他ユーザ共有したい
・ アップロードされたファイルを転送したい

というような事をしたい場合、Salesforceの標準では対応できないのでApexでの開発が必要となることが多くなるかと思います。

しかし、実際にApexを使用してファイルを操作しようとしたところ、途端に面倒くさいことになりました。
見た目は簡単、実際は面倒ということで、以下の操作をApexで行う場合の注意点についてまとめてみました。
・コンテンツドキュメントの作成
・ファイルの共有
なお、この記事の内容については簡単なApexであれば書いたことがある、という方を前提に記載しています。

コンテンツドキュメントの作成

Salesforceでファイルを保存するとき

Salesforceの画面からファイルを保存するとき、以下のような画面からファイルをアップロードします。

●アップロード前

●アップロード後

Salesforceのファイル保存場所

Salesforceで、ファイルはコンテンツドキュメントというオブジェクトで保持されています。
コンテンツ ≒ ファイルです。

ただ、このオブジェクトをApexで作成する・共有する、などの操作するにあたってはちょっとしたコツのようなものが必要です。
「コツ」というのは何かということですが、コンテンツは複数のオブジェクトから構成されていることや、 SOQLにこのオブジェクト特有の例外ルールがあったりするので、一般的な標準/カスタムオブジェクトとは違った対応が必要です。

Classic(添付ファイル)とLightning(ファイル)では保持するオブジェクトが若干異なるのですが、 今回はLightningとして、Apexテストクラス内でファイルを作成してみようと思います。
なお、私が実際に試してみたのはSummer ’21です。

コンテンツのオブジェクトの登場人物

コンテンツのオブジェクトをE-R図で表現されたものは公式のdeveloperAPIページで紹介されていて、以下の通りです。

●コンテンツのオブジェクト
https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_erd_content.htm

…といってもこれを一からかみ砕いていると日が暮れてしまうので、コンテンツ≒ファイルを保存する際に特に重要な2オブジェクトについて記載します。

① ContentDocument

添付ファイルの保存先です。ただし、ファイルを管理する親オブジェクトで実際のファイルデータの自体はここには持っていません。

② ContentVersion

実際のファイルデータ(バイナリデータ:Blob)が格納されます。
1つのバージョンで1レコードです。
最新のバージョンかどうか、アーカイブされているかどうかのフラグ項目も持っています。

1.Apex(テストクラス含む)でのコンテンツデータの作り方

実際にApexでファイルデータを作るにあたっての注意としては、 ContentDocumentにはApex操作では直接Insertできない
という点です。

そのため、ファイルデータを作る際にはまずConentVersionを生成する必要があります。
※ConentVersionを作成すると、自動的にContentDocumentが生成されます。
子のほうを作って、自動で親を生むという不思議な実装をします。これが「コツ」です。

以下のようなイメージです。

/**
* ContentDocumentを作成するクラス
*/
public class XXXXClass {

// ファイル:ContentDocument
public ContentDocument conDoc;

     /**
     * コンテンツドキュメント作成
     * @param targetId 作成したい対象のレコードのSalesforceID
     * @return ContentDocument コンテンツドキュメント
     */
    public ContentDocument createContentDocument(Id targetId) {
        if ( this.conDoc != null ) {
            return this.conDoc;
        }
        // ContentDocumentは直接作成不可なので、ContentVersionを作成しています。
        ContentVersion conVer = new ContentVersion(
            Title = 'TestContent'
            , PathOnClient = 'TestContent.txt'
            , VersionData = Blob.valueOf('TestContentData')
            , OwnerId = UserInfo.getUserId()
            , FirstPublishLocationId = targetId
        );
        insert conVer;

        // 生成されたContentDocumentを取得してリターン
        for ( ContentDocument rec : [ Select Id, Title From ContentDocument Where LatestPublishedVersionId = : conVer.Id ] ) {
            this.conDoc = rec;
        }
        return this.conDoc;
    }

}

実際に実行した結果が以下です。

XXXXClass test = new XXXXClass();
test.createContentDocument('a0G6F00002dAgSLUA0');

以下のように作成されました。

制限

コンテンツを作成するにあたって、Salesforceでは以下の制約があります。

以下、コンテンツのバージョンの更新 から抜粋

ーーー
Contact Manager Edition、Group Edition、Professional Edition、Enterprise Edition、Unlimited Edition、および Performance Edition を使用するお客様は、24 時間につき最大で 200,000 件の新しいバージョンを公開できます。
Developer Edition とトライアル版のお客様は、24 時間につき最大で 2,500 件の新しいバージョンを公開できます。
ーーー

この記載で注意しないといけないのは、これはテストクラスで作られたものも合算されます。
しかもsandboxは「24 時間につき最大で 2,500 件」です。(サポート連絡で増やすことはできる)

ですので、テストクラスではなるべくファイルの実体を不用意に作らないような工夫が必要です。
※テストでは1つのコンテンツインスタンスを使いまわす、など。

2. ファイルの共有設定

ファイルの共有設定

ファイルの共有とはそもそも何か、ということですが画面で言うと以下の部分です。
ファイルについて、設定したユーザやchatterグループに対して閲覧可能か、コラボレータ(閲覧 & 更新)権限を与える設定です。

コンテンツのオブジェクトの登場人物

ここでの登場人物は今までの①、②に加えて③の3オブジェクトです。

① ContentDocument
② ContentVersion
③ ContentDocumentLink (NEW)

ContentDocumentLinkオブジェクトは、ContentDocumentオブジェクトと紐づけたいオブジェクトを
リンクするためのオブジェクトになります。
上の画像の場合、ユーザオブジェクト(開発ユーザ)とContentDocumentオブジェクトを紐づけていることになります。

Apex(テストクラス含む)でのコンテンツデータの作り方

実際にApexでファイルデータを作るにあたっての注意としては、ContentDocumentにはApex操作では直接Insertできないという点です。
…というところまではここまでの説明と同じなので割愛します。

上で作成したApex上のテストクラスデータに対して、以下のような処理を加えます。

// 上で既にContentDocument、ContentVersionを作成しているとして

// ShareType = 'C'(コラボレータ)を付与
ContentDocumentLink link
      = new ContentDocumentLink(ContentDocumentId = contentDocument.Id,
                                LinkedEntityId = user.Id,
                                ShareType = 'C', 
                                Visibility = 'AllUsers');
insert link;

以下のように、アクセス権を付与できます。

この場合、ContentDocumentIdに対象となるcontentDocumentのIdを格納して、LinkedEntityIdには紐づけたいオブジェクトのIdを格納します。
※上の図の場合は、開発ユーザのユーザIDを格納することになります。
注意点としては、この際にContentDocumentIdとLinkedEntityIdの2つセットで一意制約となるようにすることです。

例えば、以下のようなことをするとエラーになります。

// 同一ユーザに異なるshareTypeは付与できない

// ShareType = 'V'(閲覧者)を付与
insert new ContentDocumentLink(ContentDocumentId = contentDocument.Id,
                                       LinkedEntityId = user.Id,
                                       ShareType = 'V', 
                                       Visibility = 'AllUsers');

// ShareType = 'C'(コラボレータ)を付与
insert new ContentDocumentLink(ContentDocumentId = contentDocument.Id,
                                       LinkedEntityId = user.Id,
                                       ShareType = 'C', 
                                       Visibility = 'AllUsers');

制限

制限も上で記載したことと同じです。
強いて言えば、共有をApexで記述しようとすると強制的に
・ContentDocument
・ContentVersion
・ContentDocumentLink
の3つを融合して処理することになるので、ガバナ制限には特に神経質になる必要があります。
※そもそもこれはApexだけではなくて、プロセスビルダーなどでコンテンツを操作するとき全般に言えますが。

3.まとめ

ファイルをApexで操作する際には、このようにいくつか注意する必要があるので大変ですが、劇的な機能の向上が見込めるため一度試してみていただければ思います。