10

We've been working with Attachment records on our custom objects, and we now need to migrate to Files (you know, that related list that shows up everywhere).
We have a lot of trigger logic on Attachment, and I need to get the exact same logic to work on Files (update fields of certain parent objects, prevent duplicates on some parent objects, prevent users from deleting other user's attachments/files...).
The problem is that the 3 relevant objects (ContentVersion, ContentDocument and ContentDocumentLink) are illogical, no single object has all the data I need or is triggered in a sane manner. To try and understand I put some trace flages (System.debug()) in the triggers, and I do not understand the results.

 if (trigger.isBefore){
  if (trigger.isUpdate){
    for (ContentVersion cv : trigger.new){
        System.debug('Content_Version_Trigger BEFORE UPDATE fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
  else if (trigger.isInsert){
      for (ContentVersion cv : trigger.new){
        System.debug('Content_Version_Trigger BEFORE INSERT fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
  else if (trigger.isDelete){
      for (ContentVersion cv : trigger.old){
        System.debug('Content_Version_Trigger BEFORE DELETE fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
}
else if (trigger.isAfter){
  if (trigger.isUpdate){
    for (ContentVersion cv : trigger.new){
        System.debug('Content_Version_Trigger AFTER UPDATE fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
  else if (trigger.isInsert){
      for (ContentVersion cv : trigger.new){
        System.debug('Content_Version_Trigger AFTER INSERT fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
  else if (trigger.isDelete){
      for (ContentVersion cv : trigger.old){
        System.debug('Content_Version_Trigger AFTER DELETE fired at: '+Datetime.now());
        System.debug(cv);
      }
  }
}

The same code (with different objects, obviously) appears in the ContentVersion, ContentDocument and ContentDocumentLink triggers.

The relevant output is too large to post here.
Debug log from inserting one file
Debug log from updating the description of one file
It looks like the records are being created, modified and deleted in quick succession. Why?
What kind of effect will this have on my triggers? It seems as if I cannot reliably catch any single event, it's all or nothing. That of course, won't work, because I need different logic for different actions.
EDIT: I noticed that there was a bug in the trigger output. I fixed the trigger code and relinked the new output above. There are no deletes, but it looks like multiple ContentDocumentLinks are created, and the ContentVersion is updated accordingly.

1 Answer 1

21

Alright, I spent a couple of days going through it all, and the short answer is: it's complicated. Depending on what you want your triggers to do, you may need to have different triggers on different objects, ContentVersion, ContentDocument and ContentDocumentLink. Below find the long answer.

It turns out that the debug logs are different depending on whether you are using Salesforce Classic or Lightning Experience. Why? I don't know. (Third base!) Here is a brief summary of the chain of events when uploading (inserting), editing (changing the Description) and deleting a file.

Classic - INSERT:
    ContentVersion          before  insert - no ContentDocumentId
    ContentDocumentLink     before  insert - LinkEntity = current user
    ContentDocumentLink     after   insert - has Id
    ContentVersion          after   insert - has Id, ContentDocumentId
    ContentDocumentLink     before  insert - LinkEntity = object
    ContentVersion          before  update - Publish status changed from R to P
    ContentVersion          after   update - 
    ContentDocumentLink     after   insert - has Id
    
Classic - UPDATE:
    ContentVersion          before  update
    ContentDocument         before  update
    ContentVersion          after   update
    ContentDocument         after   update
    
Classic - DELETE:
    ContentDocument         before  delete
    ContentDocument         after   delete
    
LX - INSERT:
    ContentVersion          before  insert - no ContentDocumentId
    ContentDocument         before  insert
    ContentDocument         after   insert
    ContentDocumentLink     before  insert - LinkEntity = current user
    ContentDocumentLink     before  insert - LinkEntity = object
    ContentDocumentLink     after   insert - LinkEntity = current user
    ContentDocumentLink     after   insert - LinkEntity = object
    ContentDocument         before  update - Publish status changed from U to P
    ContentDocument         after   update
    ContentVersion          after   insert
    
LX - UPDATE:
    ContentVersion          before  update
    ContentDocument         before  update
    ContentDocument         after   update
    ContentVersion          after   update
    
LX - DELETE:
    ContentDocument         before  delete
    ContentDocument         after   delete

It immediately becomes obvious that for some reason the Classic logs don't have insert events on ContentDocument, and that's part of what threw me off. The other part is that for some reason, for a couple of days, uploading a file and attaching it to a record were 2 discrete steps in LX. Now it is back to being 1 step, I don't know what changed. (I'm still looking into that and will update this answer if I ever find out).

The next most obvious conclusion is that if I want my triggers to have any effect on the parent record of the file, then I need to capture the before insert trigger on ContentDocumentLink.
If I want to capture edit events of the actual file then I need to capture the before update trigger on ContentVersion.
And if I want to capture before or after delete events, then I need to capture them on the ContentDocument trigger.

I use trigger handler classes for all my objects (one for each object), so in this case I created a single handler class for all 3 objects, and then the various triggers call the correct function from the correct event.

A final note: it's very important to make sure that the record(s) you have captured in the trigger are the ones you want to work on, since multiple insert or update events can happen in a single transaction, to the same record, but not necessarily when you want it to. For example, the before insert on ContentDocumentLink. Sometimes the LinkedEntity is the user, and sometimes the record you want to edit. So make doubly certain!

1
  • Thanks very much for your sharing. It saved me a lot of time to figure it out myself.
    – Xin
    Jul 28, 2022 at 2:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .