帳票出力先をSalesforceのファイルからBoxに切り替えてみた

はじめに

業務で写真や帳票を保存する機会が多いお客様へ、Boxの導入を支援しました。
元々は帳票ツールを用いて、Salesforceのレコードの詳細ページに配置しているファイルに帳票を出力していたのですが、Boxの導入と同時に帳票の出力先をBoxのフォルダへと切り替えたいというご要望があったので、そちらの実装方法について共有します。
※本記事は、Box for Salesforceの概要を理解している方向けです。

Boxとは

Boxとは、世界でトップレベルのシェアを誇るクラウドストレージサービスです。
Boxはいつでもどこでもアクセス可能で、時間や場所を気にすることなくデータの閲覧や編集などができます。
また、Box for SalesforceというSalesforceの中にBoxを埋め込むことを可能にするソリューションもあり、Salesforceのレコード内からBoxに保存されたコンテンツに対してより素早くアクセスし、共有することが可能になるため、業務効率が大幅に向上します。

Box for Salesforceの基本的な使い方については、公式のサポートページなどをご覧ください。
https://support.box.com/hc/ja/sections/21356626314643-Box-for-Salesforce

やりたいこと

元々は帳票ツール(OPROARTS Connector)から商談レコードのファイルに帳票を出力していますが、これをBoxの商談レコードフォルダへの出力に変更するのが今回の目的です。
Boxのフォルダに出力する方法は2つあります。
1つ目は帳票ツールからBoxへ帳票を出力させる方法で、
2つ目は帳票ツールから商談レコードのファイルに帳票を一旦出力させて、そこからBoxの商談レコードフォルダに移動させる方法です。

案1案2
メリット帳票を直接Boxに出力できるApexの実装のみで済む
デメリットApexの実装のみではなくVisualforceの改修も必要になる一旦帳票をSalesforceのファイルに出力する分の段階が増える

現状は、帳票ツールから生成されるVisualforceのコードを元にVisualforceページを作成し、カスタムアクションを作成することで、Salesforceのファイルに出力させています。
そのため、案1ではVisualforceページの改修が必要になりますが、今回はApexの実装のみで済ませたいため案2の方法を採用します。

実装方法

帳票ツールで出力した帳票が、Salesforceのファイルに保存されるタイミングをトリガにしてApexで処理を書きたいのですが、ここで
(1)Salesforceのファイル関連の知識前提
 ①AttachmentオブジェクトとDocumentオブジェクトの理解
 ②Salesforceのファイルのオブジェクト構成についての理解
(2)Boxの処理の実装方法
 ①Boxへの出力の仕方
 ②Boxのレコードフォルダの指定の仕方
の大きく2点についての情報が必要となります。

(1)Salesforceのファイル関連の知識前提

まずは商談レコードのファイルに出力した帳票が、Salesforceでどのように保存されているのかを理解していきます。

①AttachmentオブジェクトとDocumentオブジェクトの理解

ユーザがアップロードしたファイルを表すオブジェクトに、AttachmentとDocumentの2つのオブジェクトがあります。
両者の違いは以下の通りです。

AttachmentDocument
説明ユーザが親オブジェクトにアップロード
および添付したファイルを表します。
ユーザがアップロードしたファイルを表します。
Attachment レコードと異なり、ドキュメントは
親オブジェクトに添付されません。

今回は、3. やりたいこと で述べている通り、商談レコードのファイルに出力された帳票をBoxに送りたいため、親オブジェクトに添付されないDocumentオブジェクトではなく、親オブジェクトに添付したファイルを表すAttachmentオブジェクトについての基本知識が必要になります。

(2)-①Boxへの出力の仕方 でAttachment型の変数が必要になるため、必須項目を確認していきます。
公式ドキュメントを目にしたところ、以下の3つが必須項目となります。

項目説明
Body必須。符号化されたファイルデータ。
Name必須。添付ファイルの名前。最大 255 文字です。表示ラベルは File Name です。
ParentId必須。添付ファイルの親オブジェクトの ID。

参考:https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_attachment.htm

②Salesforceのファイルのオブジェクト構成についての理解

次に上記の3項目に何を値として設定するかを決める必要があります。
ここでSalesforceのファイルへのオブジェクト構成への理解が必要となります。

Salesforceのファイルのオブジェクト構成については、当社ブログに別記事があります。
【Tech-Tech】Salesforceのコンテンツドキュメントを、Apexクラスで操作してみた

詳細は上記の記事をご覧いただきたいのですが、Salesforceでファイルはコンテンツドキュメントというオブジェクトで保持されており、その中でも特に重要な3オブジェクトがあることが記載されています。
・ContentVersion:
https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentversion.htm
・ContentDocumentLink:
https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentdocumentlink.htm
・ContentDocument:
https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentdocument.htm


これら3つのオブジェクトの中に該当する項目があると思うので探したところ、以下のようになりました。

Body項目

オブジェクト項目項目の説明
ContentVersionVersionDataメモのコンテンツまたは本文。

Name項目

オブジェクト項目項目の説明
ContentVersionTitleドキュメントのタイトル。
ContentDocumentTitleドキュメントのタイトル。
ContentVersionFileExtensionドキュメントのファイル拡張子。
ContentDocumentFileExtensionドキュメントのファイル拡張子。

ParentId項目

オブジェクト項目項目の説明
ContentDocumentLinkLinkedEntityIdリンクしたオブジェクトの ID。

Name項目に関しては、タイトル+拡張子 の形で2つの項目が必要となるため、Title項目とFileExtension項目の2つを記載しています。これらの項目はContentVersionとContentDocumentの2つのオブジェクトのいずれかから取得が可能です。
Body項目はContentVersionのVersionDataを、ParentId項目はContentDocumentLinkのLinkedEntityIdをセットします。

(2)Boxの処理の実装方法

Boxの処理を書くにあたって、選択肢が3つあります。

手段説明
Salesforce Developer ToolkitSalesforceにBox for Salesforceをインストールしていれば使用することができます。
利用可能メソッドに記載がある処理に関しては実装が可能です。
https://ja.developer.box.com/guides/tooling/salesforce-toolkit/methods/
Box Salesforce SDKSalesforceに別途SDKをインストールする必要がある上に、Boxへの認証のための
処理を記述する必要があります。(今回は説明を省きます。)
しかし、Toolkitよりもできることが多いです。
https://github.com/box/box-salesforce-sdk/tree/master/doc
Box REST APIBoxにもREST APIが用意されています。
Boxへの認証のための処理を記述する必要があります。レスポンスはJSON形式です。
公式のドキュメントにできることが記載されています。
https://ja.developer.box.com/reference/

Apexで簡単に処理を書けるのは、Salesforce Developer Toolkit(以後Toolkit)とBox Salesforce SDK(以後SDK)の2つになるので、今回はこの2つに絞ります。

①Boxへの出力の仕方

ToolkitとSDKの中にファイルのコピーやアップロードが可能なメソッドはないか確認したところ、両方に該当のメソッドがありました。

手段メソッド説明使い方
ToolkitcreateFileFromAttachment(
Attachment, String, String, String)
ファイルを作成box.Toolkit boxToolkit = new box.Toolkit();
boxToolkit.createFileFromAttachment(
attatchment, 'New_File_Name’, 'FOLDER_NAME’
'New_File_Name.jpg’)
SDKcopy(BoxFolder) または
copy(BoxFolder, String)
ファイルをコピーBoxFolder rootFolder = BoxFolder.getRootFolder(api);
BoxFile file = new BoxFile(api, 'file-id’);
BoxFile.Info copiedFileInfo = file.copy(rootFolder, 'New Name’);
SDKuploadFile(Attachment, String) または
uploadFile(Document, String)
ファイルをアップロードBoxFolder rootFolder = BoxFolder.getRootFolder(api);
BoxFile.Info fileInfo = rootFolder.uploadFile(myAttachment, 'New_File_Name.jpg’);

まず、ToolkitのcreateFileFromAttachment()メソッドは、第一引数でBox内のファイルに変換される添付ファイルをAttachment型で指定します。第二引数では新しいファイルの名前を指定します。省略可能で、値が渡されなかった場合は添付ファイルの名前が使用されます。第三引数では添付ファイルの配置先であるBoxフォルダIDを指定します。省略可能で、値が渡されなかった場合はファイルは添付ファイルのparentIdに当たるレコードに関連付けられているフォルダに配置されます。レコード固有のフォルダが存在していない場合は作成されます。第四引数ではアクセストークンを指定します。指定した場合は、Box APIコールにその値が使用されます。省略可能で、値が渡されなかった場合はデフォルトアカウントの資格情報が使用されます。

次にSDKのメソッドについてです。
copy()メソッドは、第一引数でBoxフォルダをBoxFolder型で指定します。コピーするファイル名を変更したい場合は、第二引数でファイル名を指定する形です。また、コピーするファイルをBoxFile型で指定する必要があるので、Box内でファイルをコピーする際に使うメソッドだということが分かります。
一方でuploadFile()メソッドは、第一引数でアップロードするファイルをAttachment型かDocument型で指定します。こちらもコピーするファイル名を変更したい場合には、第二引数でファイル名を指定します。また、出力先となるBoxのフォルダを指定するためのBoxFolder型の変数も必要なことがわかります。
※"api"の記載部分には、ApexでBox SDKの使用を可能にするためにBoxへの認証の際にBoxApiConnectionクラスのインスタンスを生成します。
https://ja.developer.box.com/guides/authentication/tokens/sdks/

以上のことから、ToolkitとSDKの両方で実装が可能なことが分かりました。

②Boxのレコードフォルダの指定の仕方

先に述べた通り、今回はSalesforceの商談レコードに紐づくBoxの商談レコードフォルダを出力先に指定します。ToolkitとSDKの中にSalesforceレコードのフォルダを作成できるようなメソッドはないか確認したところ、SDKにはありませんでした。
一方で、Toolkitにはそういったメソッドが用意されていたため、Toolkitを使用して実装することにします。該当のメソッドは以下となります。

手段メソッド説明使い方
ToolkitcreateFolderForRecordId(Id, String, Boolean)Salesforceのレコードに紐づくBoxフォルダの作成box.Toolkit boxToolkit = new box.Toolkit();
boxToolkit.createFolderForRecordId(recordId, null, true)

戻り値:string型
・作成されたフォルダのBoxフォルダIDが返されます。
・フォルダが作成されなかった場合はnullが返されます。この場合、mostRecentErrorで詳細を確認してください。
・SalesforceレコードがすでにBoxフォルダに関連付けられている場合、既存のBoxフォルダIDが返されます。

ファイル格納先のフォルダを作成するのにToolkitの使用が必要なことが分かったので、ファイルのアップロードについてもToolkitのcreateFileFromAttachment()メソッドを採用することにします。

③commitChanges()メソッド

Toolkitでは、すべてのフォルダ/コラボレーション操作が完了した後、毎回例外なくcommitChanges()メソッドを呼び出す必要があります。
https://ja.developer.box.com/guides/tooling/salesforce-toolkit/methods/#commitchanges

ガバナ制限

今回は1つのファイルをBoxにアップロードするだけですが、処理を行うファイルのサイズやデータの数によっては
ヒープサイズに注意する必要があります。
また、今回はSalesforce SDKを使用する際に必要となるBoxへの認証処理についての説明を省きますが、コールアウト数についても気を付ける必要があります。
https://developer.salesforce.com/docs/atlas.ja-jp.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm

実際に書いてみる

先にも述べましたが、Boxのフォルダへの出力方法としては、一旦Saleforceのファイルに帳票を出力させて、そのあとにBoxのフォルダへファイルを移動させれば実現できると考えました。
そのため今回は、ContentVersionのApexトリガからBoxへのアップロード処理を書いているApexのクラスを呼ぶ方法で書いてみようと思います。
また、Boxへのアップロード処理の後にSalesforceのファイルから帳票を削除しています。

ここで、1点注意点があります。
商談レコードの詳細ページのファイルにアップロードしたときのContentDocumentLinkレコードは、①所有しているユーザレコードとの紐づき②商談レコードとの紐づき の計2レコードが作成されます。今回はファイルが商談との紐づきの場合のみBoxにアップロードしたいため、ユーザとの紐づきの情報を持つContentDocumentLinkレコードの場合はアップロード処理を行ってはいけません。
また、Salesforce上で他のオブジェクトのファイルへアップロードした場合、その情報をもとにファイルをBoxにアップロードするとエラーとなってしまうため、こちらも処理を行ってはいけません。
上記を回避するよう、ファイルに紐づくオブジェクトが商談の場合のみ処理を行うように実装する必要があります。

trigger ContentObjectTest on ContentVersion (after insert) {

    ContentHandlerTest.uploadFile(Trigger.New);
}
public with sharing class ContentHandlerTest {

    public static void uploadFile(List<ContentVersion> cvList){

        //Salesforce Developer Toolkitのインスタンス生成
        box.Toolkit boxToolkit = new box.Toolkit();

        //コンテンツドキュメントのIdリスト
        List<Id> cdIdList = new List<Id>();
        for(ContentVersion cv : cvList){
            cdIdList.add(cv.ContentDocumentId);
        }
        //コンテンツドキュメント取得
        List<ContentDocument> cdList = [SELECT Id FROM ContentDocument WHERE Id IN :cdIdList];
        //コンテンツドキュメントのマップに格納
        Map<Id, ContentDocument> cdMap = new Map<Id, ContentDocument>();
        for(ContentDocument cd : cdList){
            cdMap.put(cd.Id, cd);
        }
        //コンテンツドキュメントリンク取得
        List<ContentDocumentLink> cdlList = [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE ContentDocumentId IN :cdIdList];
        //削除するコンテンツドキュメント
        List<ContentDocument> deleteCDList = new List<ContentDocument>();

        for (ContentVersion cv : cvList) {
            
            //該当コンテンツドキュメントの取得
            ContentDocument LinkedCd = cdMap.get(cv.ContentDocumentId);

            for(ContentDocumentLink cdl : cdlList){

                if(cdl.ContentDocumentId <> LinkedCd.Id) continue;

                //商談以外のオブジェクトとの紐づきのレコードの場合は処理に進まない。
                Schema.sObjectType entityType = cdl.LinkedEntityId.getSObjectType();
                if(entityType <> Opportunity.sObjectType) continue;

                //ファイル名
                string fileName = cv.Title + '.' + cv.FileExtension;
                //ファイルの中身
                Blob fileBody = cv.VersionData;
                //紐づいているレコードのID
                string linkedEntityId = cdl.LinkedEntityId;
                //作成したBoxレコードフォルダのIDを取得
                string outputDestFolderId = boxToolkit.createFolderForRecordId(linkedEntityId, null, true);
                //Attachmentクラスに変換
                Attachment myAttachment  = new Attachment(Name = fileName, Body = fileBody, ParentID = linkedEntityId);
                //ファイルをBoxに作成
                string createFileId = boxToolkit.createFileFromAttachment(myAttachment, fileName, linkedEntityId);
                //Boxに作成したコンテンツを削除リストに追加
                deleteCDList.add(LinkedCd);
            }
        }

        boxToolkit.commitChanges();

        //削除リストの削除
        delete deleteCDList;
    }
}

まとめ

ストレージ確保のために既にBoxを導入済みの方も、これから導入する方もいるかと思います。
今回紹介したBoxのメソッドは3つのみですが、他にも多くのメソッドが用意されているため、工夫すればできることの幅が広がりそうです。
興味がある方は紹介していないメソッドについても実際に触ってみてはいかがでしょうか。