I am looking to associate ContentDocuments to Product2 records based on values in the ContentVersion record. For example I am uploading a new File record in Salesforce that is a document about Product A. When the record is created it will create a ContentDocument and ContentVersion. In the ContentVersion there are two custom fields called "Product Family" and "Product Type". On Product2 there are also two custom fields called "Product Family" and "Product Type".

I would like to create a ContentDocumentLink record that links the document to the product if the fields match up.

The apex trigger that I am writing right now is as follows (I'm new to Apex!)

trigger documentLinking on ContentVersion (after insert, after update) {

    //Build a list of ContentVersion Ids that do not have a blank Product Family or Product Type for records in trigger.new
    List<Id> listOfContentVersionId = new List<Id>();
    List<String> listOfProductFamily = new LIst<String>();
    List<String> listOfProductType = new List<String>();
    for(ContentVersion cv : Trigger.new) {
        if(cv.Product_Family__c != null && cv.Product_Type__c != null)

            System.debug('List size' + strings.size());

    if(listOfContentVersionId.size() > 0)
        //Query for Product records that have the same Product Family and Product Type as the document that is being added/updated
        List<String> matchingProducts =    [SELECT Id
                                        FROM Product2
                                        WHERE Product_Family__c = :cv.Product_Family__c
                                        AND Product_Type__c = :cv.Product_Type__c];

                                        System.debug('List size' + strings.size());

    //Pull in the matching Product records to then create a ContentDocumentLink record to them
    for(Product p : matchingProducts){
        ContentDocumentLink cdl = new ContentDocumentLink(
            ContentDocumentId = cv.ContentDocumentId,
            LinkedEntityId = matchingProducts.Id,
            ShareType = 'V'

        insert p; 
  • Welcome to SFSE! As far as first questions go, this one is pretty good! You've given us a clear explanation of your objective, and provided the work you've done so far. The one thing I'd add to your question is an explanation of where you're stuck. Are you getting an error? Is your trigger not doing what you think it should be? What is the result of running your trigger (and how does it differ from what you expect)? That kinda thing. You can edit your question to include additional details.
    – Derek F
    Commented Aug 22, 2019 at 17:12
  • One red flag I see is that you have a DML operation in a loop. Queries and DML are two of the big things that you don't want to see in a loop because that's a recipe for exceeding some of the governor limits that Salesforce enforces per-transaction. In this particular case, you'd want to create a List somewhere outside of your loop, add records to the list inside of your loop, and then finally perform the DML operation on the entire list (outside of the loop).
    – Derek F
    Commented Aug 22, 2019 at 17:16

1 Answer 1


Looks like you are on the right track for the most part. One gotcha with filtering a query with multiple lists is that even if you're using AND to tie the two conditions together, you're still going to get extra results.

If your Product Family list is {'a', 'b', 'c'}
And your Product Type list is {'1', '2', '3'}
You won't just get records back for a & 1, b & 2, and c & 3

Instead, you basically get the cartesian product. a & 1, a & 2, a & 3, b & 1, ..., c & 2, c &3. In cases like these, it's hard to do all of the work with just the SOQL query alone. Some extra processing in Apex is required.

The other issue I see is that you aren't iterating over your ContentVersion records when creating your ContentDocumentLink records. This is likely throwing an error (the variable cv is out of scope). If you would resolve the immediate issue, you'd only be generating ContentDocumentLink records for the last record in trigger.new.

The big thing your code is missing is a way to tie the Product2 and ContentVersion records together so that, given one, you can find the other.

The naive approach would be a nested loop like this

for(Object1 rec1 :obj1List){
    for(Object2 rec2 :obj2List){
        if(rec1.fieldA == rec2.fieldA && rec1.fieldB == rec2.fieldB){
            // we have a match!
            // do some work

The issue with that approach is that it causes you to run the if statement N * M times (where N and M are the number of records in obj1List and obj2List respectively). For a relatively modest number of records, say 200 of one and 400 of the other, you end up executing that if statement 200 * 400 = 80,000 times. It's also likely that the if statement will evaluate to false more often than not. Lots of wasted work being done.

A better approach is to use a Map. The combination of Product Family and Product Type can act as a composite key for your map.

Map<String, Product2> familyAndTypeToProductMap = new Map<String, Product2>();

for(Product2 prod :matchingProducts){
    familyAndTypeToProductMap.put(prod.ProductFamily + prod.ProductType, prod);

Once you have that, you should be able to loop over trigger.new, recreate the composite key, and check to see if you have a match.

for(ContentVersion cv :trigger.new){
    String compositeKey = cv.Product_Family__c + cv.Product_Type__c;

        // we found a match!
        // create the ContentDocumentLink record

Yes, you still have 2 loops here, but you only iterate N + M times total (and 600 iterations is much more manageable than 80,000). Also, we don't need to loop over the ContentVersion record more than once to be able to determine if there is a match or not.

You must log in to answer this question.

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