Salesforceのファイルのオブジェクト構成とApexの書き方

この記事は [Salesforce Platform Advent Calendar 2020 - Qiita](https://qiita.com/advent-calendar/2020/salesforce-platform) 第14日目の投稿および
Salesforce 開発者向けブログ投稿キャンペーンへのエントリー記事です。

# はじめに

**Salesforceのファイルについてまとめてみました**

**・「メモ&添付ファイル」と「ファイル」の違い**
**・ContentDocument と ContentVersion のオブジェクト構成**
**・レコード添付時のオブジェクト構成の違い**
**・レコードIDから ContentVersion データ一覧の取得方法**
**・Apex でのファイル insert の違い**
**・Apex でファイルをコピーする方法**
**・ファイルをダウンロードするURLを生成**

# 「メモ&添付ファイル」と「ファイル」の違い
まず、レコードにファイルを添付するには以下の2種類の方法があります。
Classic 世代の古くから存在する「**メモ&添付ファイル**」と、 LightningExperience 世代の新しい「**ファイル**」の2種類が存在します。

### ファイルアップロード時の格納場所の違いについて
アップロードするインターフェースとアップロードする場所によって格納されるオブジェクトが異なります。

|インターフェース|アップロード方法|格納されるオブジェクト|
|:-:|:-:|:-:|
|Classic|メモ&添付ファイル|Attachment|
|Classic|ファイル|ContentDocument および ContentVersion|
|Lightning Experience|メモ&添付ファイル|ContentDocument および ContentVersion|
|Lightning Experience|ファイル|ContentDocument および ContentVersion|

「メモ&添付ファイル」から Attachment に格納というイメージではなく、**以下の Classic のインターフェースの際に Attachment に格納**されるイメージかと思います。
![Attachmentインターフェース.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533011/9e7c4f76-5275-5cf1-9248-299c92df4bb2.png)

# ContentDocument と ContentVersion のオブジェクト構成
基本的には **Classic では「Attachment」**、**LightningExperience では「ContentDocument と ContentVersion」**となっているため、今後は以下の Content 関連が重要となってきます。
![コンテンツのオブジェクト2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533011/908bbf2b-6eda-5a69-166b-9d15956895d4.png)
【参考】[Apex 開発者ガイド：コンテンツのオブジェクト]
(https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_erd_content.htm)

**ContentDocument ではファイルのコンテンツ情報**を保持し、 **ContentVersion ではバージョン情報**を保持します。ファイルの**実データは ContentVersion** 内で保持しています。
**ライブラリを使用する場合は、 ContentWorkspace との関連を ContentWorkspaceDoc** で保持します。
**レコードへの関連は ContentDocumentLink** で保持します。

# Attachment の時とのレコード添付時の構成の違い
![Attachmentでのオブジェクトのファイル添付の構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533011/da9fe694-fc73-b923-1fe1-8dfdad6ad3d1.png)
Attachment の際はシンプルで、**オブジェクトレコードの関連やファイルデータの実態は Attachment オブジェクト内で全て完結**します。

![ContentDocument および Content Version でのオブジェクトのファイル添付の構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533011/74bd99ca-899f-06c8-90fb-d32d10bc5376.png)
ContentDocument および ContentVersion の際は少し複雑で、**オブジェクトレコードの関連は ContentDocumentLink** で行い、**ファイルデータの実態は ContentVersion の VersionData** で保持します。

# レコードIDから ContentVersion データ一覧の取得方法
**レコードIDから ContentVersion データの一覧の取得方法は2パターン**

**パターン①： ContentDocumentLink から対象レコードIDを指定して取得**

```apex
SELECT ContentDocument.LatestPublishedVersionId FROM ContentDocumentLink WHRERE LinkedEntityId = '............'
```

**パターン②：対象オブジェクトの CombinedAttachment のサブクエリから取得**

```apex
SELECT Id, (SELECT Id, Title FROM CombinedAttachments) FROM Test__c WHRERE Id = '............'

/*
サブクエリで取得したIdを指定することで以下の ContentDocument が取得できるため、
LatestPublishedVersionId で ContentVersion データのIDが取得できる
*/

SELECT LatestPublishedVersionId FROM ContentDocument WHERE Id IN :idList
```
ただし、②では sobject.CombinedAttachments のようにSObject型では取得できないため、 account.CombinedAttachments のようにオブジェクトを指定しないと取得できないみたいなのであまり汎用的に使用できません。

# Apex でのファイル insert の違い
**Attachment 場合**

```apex
Attachment att = new Attachment();
att.ParentId = '............'; // レコードへの関連
att.Name = 'test.csv'; // ファイル名
att.Body = Blob.valueOf('test'); // ファイルデータ
insert att;
```

**ContentDocument および ContentVersion 場合**

```apex
ContentVersion cv = new ContentVersion();
cv.ContentLocation = 'S'; // ファイルはSalesforce上に保存
cv.PathOnClient = 'test.csv'; // ファイル名
cv.Title = 'テスト'; // ファイルタイトル
cv.VersionData = Blob.valueOf('test'); // ファイルデータ
insert cv;

// レコードへの関連
ContentDocumentLink cde = new ContentDocumentLink();
cde.ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId; // コンテンツ情報
cde.LinkedEntityId = '............'; // レコードへの関連
cde.ShareType = 'V'; // ファイル権限
insert cde;
```
**ContentDocument は直接 insert 不可**
エラー「DML operation Insert not allowed on ContentDocument」
**ContentVersion を insert すると自動で ContentDocument が insert される仕様**のようです。

# Apex でファイルをコピーする方法
### ファイルの実態もコピーする場合
```apex
// コピー元ファイル
ContentVersion origin = [SELECT Id, Title, PathOnClient, ContentDocumentId, VersionData FROM ContentVersion WHERE Id = '............'];

// ファイルを新規作成
ContentVersion cv = new ContentVersion();
cv.ContentLocation = 'S'; // ファイルはSalesforce上に保存
cv.PathOnClient = origin.PathOnClient; // ファイル名
cv.Title = origin.Title; // ファイルタイトル
cv.VersionData = origin.VersionData; // ファイルデータ
insert cv;

// レコードへの関連
ContentDocumentLink cde = new ContentDocumentLink();
cde.ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId; // コンテンツ情報
cde.LinkedEntityId = '............'; // レコードへの関連
cde.ShareType = 'V'; // ファイル権限
insert cde;
```
### ファイルの実態はコピーせず、ファイルの関連先のみを増やす場合
```apex
// コピー元ファイル
ContentVersion origin = [SELECT Id, Title, PathOnClient, ContentDocumentId, VersionData FROM ContentVersion WHERE Id = '............'];

// レコードへの関連
ContentDocumentLink cde = new ContentDocumentLink();
cde.ContentDocumentId = origin.ContentDocumentId; // コンテンツ情報
cde.LinkedEntityId = '............'; // レコードへの関連
cde.ShareType = 'V'; // ファイル権限
insert cde;
```
![ファイルコピーイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533011/86c99347-3341-8837-d11f-84e6a692efc3.png)

ファイルの関連先のみを増やす場合は、**同じコンテンツ情報を共有**しているため、「新しいバージョンをアップロード」で別ファイルデータに**バージョンを上げた際にも両方のオブジェクトのファイルデータに同期**されます。

# ファイルをダウンロードするURLを生成
**以下URLでファイルダウンロードが可能**
ContentVersion のIDをパラメータに設定し、URLにアクセスするだけでそのファイルをダウンロードすることが可能です。
``https://xxxxxxx.my.salesforce.com/sfc/servlet.shepherd/version/download/{!ContentVersionId}
``

# まとめ

Salesforceには、「メモ&添付ファイル」と「ファイル」があり、それぞれの違いがよくわからなかったり、また Attachment と Content 関連の仕組みが全然違ったりするため、備忘録も兼ねて今回はファイル関連についてまとめました。