We have a trigger on Order attachment insertion, so that a field is populated with a link to the attached documents whenever a user uploads one or multiple files to an existing Order.

We created the trigger on ContentDocumentLink object, and we started having problems because of DML Exceptions for hitting governor limits System.LimitException: Too many SOQL queries: 101, but actually the 2 queries inside the trigger are run only once. We noticed, with test class, that bulk inserting a List of files, fires the trigger as many times as the list size, instead of a single one, that is why the Exception is thrown. How can this be possible?

The trigger

trigger ContentDocumentLinkTrigger on ContentDocumentLink (after insert) {
    if(Trigger.isInsert && Trigger.isAfter){
       ContentDocumentLinkTriggerHandler.afterInsert(Trigger.oldMap, Trigger.newMap);

Trigger handler

public class ContentDocumentLinkTriggerHandler {
    public static void afterInsert(Map<Id, ContentDocumentLink> oldMap, Map<Id, ContentDocumentLink> newMap){

        Set<Id> cvIds = new Set<Id>();
        for (ContentDocumentLink dLink : newMap.values()) {

        Map<Id, ContentVersion> docVersions = new Map<Id, ContentVersion>([SELECT contentDocumentId, firstPublishLocationId, Id
                                                      FROM ContentVersion
                                                      WHERE contentDocumentId IN : cvIds LIMIT 1]); 

        List<Id> idOrders = new List<Id>();
        List<Order> ordersToUpdate = new List<Order>();
        for(ContentVersion cv : docVersions.values()){

        Map<Id,Order> updateOrder = new Map<Id,Order>([SELECT Id, customField FROM Order WHERE Id IN :idOrders ]);

        if(updateOrder.size() >0){
            for(Order o : updateOrder.values()){
                    o.customField = 'custom';
        update ordersToUpdate;

The test class

public class ContentDocumentTriggerTest {   
    static void  testingDoc(){
      ...//order creation
      List<ContentVersion> contentList = new List<contentVersion> ();
      for(Integer i = 0; i < 10; i++){
        ContentVersion contentVersion = new ContentVersion(
            Title = 'Penguins',
            PathOnClient = 'Penguins.jpg',
            VersionData = Blob.valueOf('Test Content'),
            IsMajorVersion = true,
            firstPublishLocationId = o.Id 


      insert contentList;  //This insert fires 10 times the trigger

EDIT Following the hint from @cropredy I've tried to write a test class with a mass insertion of multiple ContentDocumentLink but I'm not able to achieve it, as even with only 2 objects I get exception

System.DmlException: Insert failed. First exception on row 0; first error: FIELD_INTEGRITY_EXCEPTION, Document with ID: 0690D000000iIIQ is already linked with the entity with ID: 8010D000000QMqJ: [LinkedEntityId]

Code is as follow:

static void  DocumentAttach(){
 //...order creation  with a list of 2 orders for testing
List<Order> orders = new List<Order>();
insert orders;

String before = 'Testing base 64 encode';            
Blob beforeblob = Blob.valueOf(before);
ContentVersion cv = new ContentVersion();
cv.title = 'test content trigger';      
cv.PathOnClient ='test';           
cv.VersionData =beforeblob;         
insert cv;  //this insertion fires trigger on content document link 3 times       

List<ContentDocumentLink> cvLinks = new List<ContentDocumentLink> ();

Id cdID = [SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id =: cv.Id].ContentDocumentId;

for(Integer i = 0; i < 10; i++){
   ContentDocumentLink cdl = new ContentDocumentLink();
  cdl.ContentDocumentId = cdID ;
  cdl.LinkedEntityId = orders[i].Id;
  cdl.ShareType = 'V';

insert cvlinks; //this fires the trigger 3 times

Any clue?

  • this is most likely related to using ContentVersion.FirstPublishLocationId that behind the scenes creates a ContentDocumentLink to the Order (x 10). That operation might not be bulkified. Have your testmethod build CDL explicitly rather than implicitly – cropredy Oct 18 '19 at 22:15
  • mmm no i don't have one... i will try to find some time to write one and test again. – sissy Oct 21 '19 at 12:38
  • @cropredy i've tried to create a test class with the creation of multiple contentDocumentLink directly but i'm not able to achieve it. I always get the exception System.DmlException: Insert failed. First exception on row 0; first error: FIELD_INTEGRITY_EXCEPTION, Document with ID: 0690D000000iIIQ is already linked with the entity with ID: 8010D000000QMqJ: [LinkedEntityId] Please have a look at the added code. – sissy Oct 21 '19 at 16:20
  • 1
    Get rid of the CV.firstpublishLocationId assignment when you build the CV. – cropredy Oct 22 '19 at 1:44
  • Thank you @cropredy i've modified the code as you suggested and now i made a test inserting 10 orders, 1 contentVersion and 10 contentDocumentLinks (1 for each order). Debugging it comes out that inserting 1 contentVersion, triggers 3 times the contentDocumentLink trigger and the CdLink list insertion fires the trigger 3 times again,as it was the contentVersion. I modified the last test class,if you can look at that.Still unclear why.do you have any good reference to better understand this relations? thx – sissy Oct 22 '19 at 7:18

Let's consider a simple ContentDocumentLink trigger and helper inspector class

trigger ContentDocumentLinkTrigger on ContentDocumentLink (before insert, before update, after insert, after update) {

public with sharing class TriggerInspector {
  static Integer beforeInsertCount = 0;
  static Integer beforeUpdateCount = 0;
  static Integer afterInsertCount = 0;
  static Integer afterUpdateCount = 0;

  static Map<integer,ContentDocumentLink[]> cdlsByInvocation =
        new Map<Integer,ContentDocumentLink[]> ();

    public static void log(ContentDocumentLink[] cdls) {
        if (Trigger.isBefore) {
            if (Trigger.isInsert) {beforeInsertCount++;}
            else if (Trigger.isUpdate) {beforeUpdateCount++;}
        else if (Trigger.isAfter) {
            if (Trigger.isInsert) {
            else if (Trigger.isUpdate) {afterUpdateCount++;}

    public static String inspect() {
        return 'beforeInsertCount:'+beforeInsertCount + '\n' +
                 'afterInsertCount:'+afterInsertCount + '\n' +
                 'beforeUpdateCount:'+beforeUpdateCount + '\n' +
                 'afterUpdateCount:'+afterUpdateCount + '\n' +
                'cdlsByInvocation:' + cdlsByInvocation;

Now, with some anonymous Apex that inserts an Opportunity, creates two ContentVersion and then inserts ContentDocumentLink (2)

delete [select id from Opportunity where name = '00Oppo'];
Opportunity o = new Opportunity(CloseDate = Date.today(), Name = '00Oppo', StageName = 'Prospecting');
insert o;

ContentVersion[] cvs = new List<ContentVersion>();
for (Integer i = 0; i < 2; i++) {
    cvs.add(new ContentVersion (
            Title = 'cv'+i,
            VersionData = Blob.valueOf('foo')
,           PathOnClient = 'foo.csv'
insert cvs;

ContentDocumentLink[] cdls = new List<ContentDocumentLink>();
for (ContentVersion cv: [select ContentDocumentId FROM ContentVersion WHERE Id IN: cvs]) {
    cdls.add(new ContentDocumentLink (
        LinkedEntityId = o.Id,
        ShareType = 'V',    
        ContentDocumentId = cv.ContentDocumentId)   
insert cdls;

What do we get?

Surprisingly, as you observed, the ContentDocumentLink trigger is invoked three times despite only a single DML insert

beforeInsertCount:3 // !?!

Invocation 1 shows that a ContentDocumentLink is autocreated with a LinkedEntityId of the running user (prefix 005). This gets created for the first ContentVersion inserted. Notice the ShareType = I (inferred permission)

1=(ContentDocumentLink:{Id=06A63000000MHQqEAO, LinkedEntityId=00536000004Ui8MAAS, 
  ContentDocumentId=069630000007z3PAAQ, ShareType=I, Visibility=AllUsers})

Then, the second ContentDocumentLink is created in a separate trigger invocation, also inferred sharetype and associated to the running user

2=(ContentDocumentLink:{Id=06A63000000MHQrEAO, LinkedEntityId=00536000004Ui8MAAS, 
  ContentDocumentId=069630000007z3QAAQ, ShareType=I, Visibility=AllUsers})

Finally, in a bulkified Trigger, the two ContentDocumentLink are associated to the Opportunity (prefix 006) using the Sharetype as specified in the DML's SObject (V)

ContentDocumentLink:{Id=06A63000000MHQsEAO, LinkedEntityId=00663000008vAJmAAM, 
ContentDocumentId=069630000007z3PAAQ, ShareType=V, Visibility=InternalUsers}, 

ContentDocumentLink:{Id=06A63000000MHQtEAO, LinkedEntityId=00663000008vAJmAAM, 
ContentDocumentId=069630000007z3QAAQ,  ShareType=V, Visibility=InternalUsers})}

So what can we conclude?

  • SFDC auto-constructs a ContentDocumentLink in a non-bulkified manner for every inserted ContentVersion linked to the running user. Hence, any ContentDocumentLink trigger is executed non-bulkified and should refrain from doing SOQL if the ShareType is I
  • SFDC does bulkified trigger on any bulkified DML of ContentDocumentLink. Here, SOQL can be safely executed.

Is the behavior any different if ContentVersion.FirstPublishLocationId is non null?

YES (sort of). Consider this code:

delete [select id from Opportunity where name = '00Oppo'];
Opportunity o = new Opportunity(CloseDate = Date.today(), Name = '00Oppo', StageName = 'Prospecting');
insert o;

ContentVersion[] cvs = new List<ContentVersion>();
for (Integer i = 0; i < 2; i++) {
  cvs.add(new ContentVersion (
        Title = 'cv'+i,
        FirstPublishLocationId = o.Id,
        VersionData = Blob.valueOf('foo')
        ,           PathOnClient = 'foo.csv'
insert cvs;


The ContentDocumentLink trigger is invoked twice, each with two records

First invocation:

  • Record[0] is a CDL linked to the runningUser , shareType = 'I' (for CV[0])
  • Record[1] is a CDL linked to the Opportunity, shareType = 'V' (for CV[0])

Second invocation:

  • Record[0] is a CDL linked to the runningUser , shareType = 'I' (for CV[1])
  • Record[1] is a CDL linked to the Opportunity, shareType = 'V' (for CV[1])

This suggests, when coding ContentDocumentLink triggers to not use FirstPublishLocationId as it is impossible to do SOQL in a bulkified manner, even by excluding shareType I records.

| improve this answer | |
  • Thank you, great answer! So if I understood well, there's no way to have a bulkified trigger when inserting multiple files to an object right? – sissy Oct 23 '19 at 14:33
  • if you use my first approach, and insert n CV, the trigger is bulkified on the n+1st iteration and you should have code to ignore the first n non-bulkified iterations as they don't build the CDL to the Opportunity – cropredy Oct 23 '19 at 21:14
  • ummm.I've tried moving the triggers on ContentVersion obj, but when uploading (manually, not with tests or code) multiple files, the triggers where not bulkified and fired 3 times on each doc (2 for shareType I, 1 for CDL). Sorry for bothering, but it's not clear what you mean. Thank you :) – sissy Oct 24 '19 at 8:02
  • @cropredy Is Salesforce aware of this issue? Because this non-bulkified behaviour is really annoying and there's nothing elegant we can do. – Willem Mulder Mar 27 at 15:40
  • @WillemMulder - I do not know if SFDC is aware of this issue -- you could atMention CA Peterson on twitter - he can be responsive to stuff like this – cropredy Mar 28 at 1:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.