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){
ContentDocumentId = cv.ContentDocumentId,
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 Aug 22 '19 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 Aug 22 '19 at 17:16

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;

if(familyAndTypeToProductMap.containsKey(compositeKey)){
// we found a match!