2023.09.07

Apexトリガを使ってファイルの作成・更新・削除を制限しよう!

はじめに

Tech Blogでたびたび取り上げている「ファイル」機能ですが、今回はレコードに対するファイルの作成・更新・削除を制御するApexトリガを作成していきたいと思います。

シナリオは以下の通りです。

【シナリオ】
 ・商談レコードに対するファイルの作成・更新・削除を想定
 ・商談のフェーズが「Closed Lost」のときにファイルを作成・更新・削除しようとするとエラーになる

ファイル関連オブジェクトの概要

Apexトリガを作成する際には、どのオブジェクトのどんなイベントでトリガを発火させるかを指定する必要があります。

ファイルの作成・更新・削除を制御するためには、どのオブジェクトにトリガを作成すればいいのでしょうか?

まずはファイル関連オブジェクトの特徴を確認しましょう。

ファイル関連オブジェクトの構成

今回、レコードに対するファイルの作成・更新・削除を行っていくうえで確認しておきたいのは以下のオブジェクトです。
ContentDocument
ファイルの情報を格納
ファイル自体は保持しない
ContentDocumentレコードを直接作成することはできない
ContentVersion ファイル自体はここに格納されている
ContentVersionレコードを作成すると、ContentDocumentレコードも同時に作成される
ContentDocumentLink ContentDocumentレコードと関連レコードのID情報を格納
ファイルとレコードの関連付けを担う
オブジェクトの構成は以下のようになっています。

各オブジェクトの特徴については、過去の記事で詳細に取り上げていますので、ぜひ参考にしていただければと思います。

ファイル関連オブジェクトに対するデータ操作

ファイル関連オブジェクトの構成を確認したところで、実際に各オブジェクトがどのように機能しているのかについてはあまりイメージがわかないのではないでしょうか?

ここからは、画面からファイルの作成・削除を行った際に、どのオブジェクトに対してデータ操作が行われているのかについて確認してみましょう。

ファイルの追加

まずは、レコードにファイルを追加してみましょう。

レコードにファイルを追加しようとすると、

すでにライブラリにアップロードされているファイルを追加することもできますし、新たにファイルをアップロードすることもできます。

商談レコードにファイルを関連付けたいわけですから、ここではContentDocumentLiinkレコードを作成するのですが、すでにライブラリにアップロードされているファイルを追加する場合は、すでに存在しているContentDocumentと商談レコードを紐づけるContentDocumentLinkを作成します。

一方で、新たにファイルをアップロードする場合は、ContentVersionを作成し、ContentDocumentを生成した後で、ContentDocumentと商談レコードを紐づけるContentDocumentLinkを作成します。

少し細かいのですが、どちらの挙動を制御したいかによって、トリガを作成するオブジェクトが変わってくる場合がありますので、注意が必要です。

ここでは新規ファイルをアップロードしてみます。

レコードにファイルが追加されましたね。

アップロードしたファイルの詳細画面を確認してみると、

共有先(ContentDocumentLink)関連リストには、レコードが2件作成されています。
1件はファイルと商談レコードを紐づけるContentDocumentLinkレコードで、もう1件はファイルをアップロードしたユーザ(所有者)を紐づけるContentDocumentLinkレコードです。

商談レコードにのみ紐づけたつもりでも、ファイルの所有者となるユーザにもContentDocumentLinkレコードが作成される点にも注意が必要です。(理由は後述します)

ファイルの削除

続いてアップロードしたファイルを削除してみましょう。

ファイルの削除と一括りに言っても、

・ファイル自体の削除
・レコードからの削除


が可能です。

まずはファイル自体の削除から確認しましょう。

ファイル自体の削除をする際には、ContentDocumentレコードが削除され、親の削除に伴って子オブジェクトのContentVersionおよびContentDocumentLiinkレコードも削除されるようです。

続いてレコードからの削除です。

こちらのファイルを商談レコードから削除してみましょう。

ファイルをレコードから削除する際には、ファイルと商談レコードを関連付けていたContentDocumentLinkレコードのみが削除されることが確認いただけたと思います。

どのオブジェクトにトリガを作成すればよいのか

では、結局のところ、どのオブジェクトにトリガを作成すればいいのでしょうか?

以下の表をご覧いただければわかっていただけるように、実は制御したい操作の内容によってトリガを作成するオブジェクトが異なります。
トリガ
イベント
オブジェクト 制御したい操作
ライブラリ上のファイルを
レコードに追加
新規ファイルを
追加
バージョンの
追加
ファイル項目の
変更
ファイルの削除 ファイルを
レコードから
削除
before
insert
Content
Document
Link
〇 〇
before
update
Content
Document
〇 〇 〇
Content
Version
〇
bafore
delete
Content
Document
〇
Content
Document
Link
〇
たしかに、商談レコードからファイルをアップロードしたり、削除したりというのはContentDocumentLinkで制御できるとしても、ファイルの更新やファイル自体の削除はContentDocumentで制御する必要があるというのは納得ですよね。

表には記載していませんが、ファイル詳細画面からの手動共有については、ContentDocumentLinkで制御することができました。

Apexトリガの作成

それでは、ここまでの内容を踏まえて実際にApexトリガを作成してみましょう。

改めてシナリオは以下の通りです。
【シナリオ】
 ・商談レコードに対するファイルの作成・更新・削除を想定
 ・商談のフェーズが「Closed Lost」のときにファイルを作成・更新・削除しようとするとエラーになる


まずは、レコードに対するファイルの新規作成および更新を制限するApexトリガを作成します。

トリガのイベントタイプはbefore update、トリガを設定するオブジェクトはContentDocumentです。

ファイルの作成と更新を制御するトリガ

trigger ContentDocumentTrigger on ContentDocument (before update) {

    //ハンドラー
    ContentDocumentTriggerHandler handler = new ContentDocumentTriggerHandler();

    if(Trigger.isBefore && Trigger.isUpdate){
        handler.beforeUpdate(Trigger.newMap);
    }
}
ContentDocumentTrigger.trigger
public with sharing class ContentDocumentTriggerHandler {
    public void beforeUpdate(Map<Id, ContentDocument> newCdMap) {

        /*
        概要:
         更新しようとしているファイルが紐づく商談のフェーズがClosed Lostか判定し、
         Closed Lostであった場合はエラーを出す
    */

        //更新対象のContentDocumentに紐づくContentDocumentLinkの取得
        List<ContentDocumentLink> cdlList = new List<ContentDocumentLink>([SELECT Id, ContentDocumentId, linkedEntityId
                                                                            FROM ContentDocumentLink 
                                                                            WHERE ContentDocumentId IN: newCdMap.keySet()]);

        //関連レコード(linkedEntityId)の抽出
        Set<Id> linkedIdSet = new Set<Id>();
        for(ContentDocumentLink cdl: cdlList) {
            linkedIdSet.add(cdl.linkedEntityId);
        }

        //関連レコード(linkedEntityId)の中から商談に紐づくレコードを抽出
        Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([SELECT Id, StageName 
                                                                FROM Opportunity 
                                                                WHERE Id IN: linkedIdSet]);
        
        for(ContentDocumentLink cdl: cdlList) {

            //関連レコードのID
            Id targetId = cdl.linkedEntityId;
            
            //関連レコードのオブジェクトタイプ
            sObjectType objType = targetId.getSObjectType();
    
            //紐づくContentDocumentレコード
            ContentDocument cd = newCdMap.get(cdl.ContentDocumentId);

            //関連レコードのオブジェクトが商談かどうか、フェーズがClose Lostかどうかを判定
            if(objType == Opportunity.sObjectType && oppMap.get(targetId).StageName == 'Closed Lost'){
                cd.addError('失注した商談のファイルは追加・更新できません。');
            }
        }
    }
}
ContentDocumentTriggerHandler.cls
ハンドラークラスでは、以下の処理を実施しています。

イベントのトリガとなったContentDocumentレコード(Trigger.newMap)から
①紐づくContentDocumentLinkを取得する
②ContentDocumentLinkに紐づく商談レコードを取得する
③その商談レコードのフェーズの値が「Closed Lost」だったらエラーを出す


ただし、前述したように、ContentDocumentLinkオブジェクトには商談に紐づくレコードとユーザに紐づくレコードが存在していますので、LinkedEntityIdのオブジェクト種別をチェックしてあげる必要があります。

上記の処理をデプロイすると、商談のフェーズが「Closed Lost」の商談レコードに対して

・ファイルの新規アップロード(ただし、指定したエラーメッセージが確認できない)
・新しいバージョンのアップロード
・ファイル詳細画面の項目の更新

を実行しようとしたときにエラーを出すことができます。

※ライブラリにアップロード済みのファイルについてはレコードに追加できてしまうため、こちらも制御する場合は、ContentDocumentLinkオブジェクトにbefore insertトリガを作成する必要があります。

ファイルの新規アップロード

新しいバージョンのアップロード

ファイル詳細画面の項目の更新

ファイルの削除を制御するトリガ

続いて、レコードに紐づいたファイルの削除を制限するApexトリガを作成します。

前述のとおり、ファイルの削除には2パターンが考えられます。

ファイル自体の削除を制御する際には、ContentDocumentを使用します。
一方で、レコードからの削除を制御する際には、ContentDocumentLinkを使用します。
トリガイベントはどちらともbefore deleteになります。

ここでは、レコードからのファイルの削除を制御するApexトリガを作成していきます。
trigger ContentDocumentLinkTrigger on ContentDocumentLink (before delete) {

    //ハンドラー
    ContentDocumentLinkTriggerHandler handler = new ContentDocumentLinkTriggerHandler();

    if(Trigger.isBefore && Trigger.isDelete){
        handler.beforeDelete(Trigger.oldMap);
    }
}
ContentDocumentLinkTrigger.trigger
public with sharing class ContentDocumentLinkTriggerHandler {
    public void beforeDelete(Map<Id, ContentDocumentLink> oldCdlMap) {

        /*
        概要:
         削除しようとしているファイルが紐づく商談のフェーズがClosed Lostか判定し、
         Closed Lostであった場合はエラーを出す
    */

        //関連レコード(linkedEntityId)の抽出
        Set<Id> linkedIdSet = new Set<Id>();
        for(ContentDocumentLink cdl: oldCdlMap.values()) {
            linkedIdSet.add(cdl.linkedEntityId);
        }
        
        //関連レコード(linkedEntityId)の中から商談に紐づくレコードを抽出
        Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([SELECT Id, StageName 
                                                                FROM Opportunity 
                                                                WHERE Id IN: linkedIdSet]);

        for(ContentDocumentLink cdl: oldCdlMap.values()) {
            
            //関連レコードのID
            Id targetId = cdl.linkedEntityId;

            //ContentDocumentLinkと紐づくレコードのオブジェクトタイプ
            sObjectType objType = targetId.getSObjectType();

            //ファイルを商談から削除
            if(objType == Opportunity.sObjectType && oppMap.get(targetId).StageName == 'Closed Lost'){
                cdl.addError('失注した商談のファイルは削除できません。');
            }
        }
    }
}
ContentDocumentLinkTriggerHandler.cls
トリガを作成するオブジェクトはContentDocumentLinkになります。

ハンドラークラスでは以下の処理を実施しています。

①削除対象のContentDocumentLinkに紐づく商談レコードを取得
②その商談レコードのフェーズの値が「Closed Lost」だったらエラーを出す


こちらは、トリガコンテキスト変数で削除対象のContentDocumentLinkレコードが容易に取得できますので、作成・更新を制御するトリガハンドラーよりもシンプルに書くことができました。

このように商談のフェーズが「Closed Lost」のレコードに紐づくファイルを削除しようとした際に、エラーを出すことができました。

最後に

今回の記事では、ファイルの作成・更新・削除を制御するApexトリガについてご紹介しました。

ファイル機能は便利な一方で、オブジェクト構成が複雑なため、苦手意識を持っている方もいらっしゃるかもしれません。
今回はシンプルに特定の商談のフェーズになった際にファイルの作成・更新・削除を制御するトリガを作成しましたが、承認申請中のレコードに対してロックをかけたり、項目の値に応じて共有を制限したりすることもでき、応用が利くトリガとなっています。

もしファイル機能についてトリガを作成する機会があった際には、参考にしていただければ幸いです。
59 件
     
  • banner
  • banner

関連する記事