[SalesForce] Convert Trigger to use Dynamic Apex

I want to convert my below trigger to use Dynamic Apex, so that I can avoid code repetition for other objects. This is what my trigger does :-

  1. I have a object called Incident__c and a custom field called Notes__c (Long Text Area).
  2. Whenever a Note get created(In the Incident__c object record), Notes__c field should be updated with Note body.
  3. When there are many Notes created, Notes__c field should append with all Note bodies.
  4. Also, when an existing 'Note' is edited, edited value should be updated in the Notes__c field.
  5. When a 'Note' is deleted, Notes__c field should also be updated according to deletion.

My trigger currently does this all. But I got another 4 objects I should support the same scenario. And all those objects have that 'Notes__c' field created. So I want to use Dynamic apex so there won't be repeated code, how to use Dynamic apex to get the APINames of objects I want and use SObject types ?

-----Helper class--------------
public with sharing class NoteTriggerHelper {
    public static void insertNotesBodyToTextField(List<Note> notes){
        Set<Id> incid = new Set<Id>();
            for(Note n : notes){
                incid.add(n.ParentId);
            }

        List<grc__Incident__c> incidents = [Select Id,Notes__c, (SELECT Id,Title, Body FROM Notes) from grc__Incident__c where Id IN :incid];

            for(grc__Incident__c incident : incidents){
                incident.Notes__c = getNotesString(incident.Notes);
            }

        update incidents;
    }


    private static String getNotesString(List<Note> notes){
        String notesString = '';

            for(Note nt : notes){
                notesString = ' seperator ' + nt.Body + '\n' +notesString;
            }
                return notesString;
    }
}

----------trigger----------------------
    trigger NoteTrigger on Note (after insert, after update, after delete) {
        if(Trigger.isAfter){
            if(Trigger.isInsert){
                NoteTriggerHelper.insertNotesBodyToTextField(Trigger.new);
            }

            if(Trigger.isUpdate){
                NoteTriggerHelper.insertNotesBodyToTextField(Trigger.new);
            }

            if(Trigger.isDelete){
                NoteTriggerHelper.insertNotesBodyToTextField(Trigger.old);
            }
        }
    }

Best Answer

I agree with @crop1645 that DLRS is a good option here, but if you must roll your own it would look something like:

Utilities

Getting Parent Ids

I consider the following utility essential. It is easy to write, easy to test, and is so ubiquitous it will add a lot of syntactic sugar over time. It also means you don't have to cache these collections. We use sets to improve compatibility with queries.

public class Pluck
{
    public static Set<Id> ids(SObjectField field, List<SObject> record)
    {
        Set<Id> ids = new Set<Id>();
        for (SObject record : records) ids.add((Id)record.get(field));
        ids.remove(null);
        return ids;
    }
    public static Set<String> strings(SObjectField field, List<SObject> records)
    {
        // etc.
    }
}

Grouping

One of the first things you will need to do is be able to split the Note records from your trigger out by parent record so you can use them to generate specific parent Notes__c values. We also tend to have a utility around for this functionality. You can support String keys in addition to SObjectField by using some sort of model class like FieldReference and method overloads.

public class GroupBy
{
    public static Map<Id, List<SObject>> ids(SObjectField field, List<SObject> records)
    {
        Map<Id, List<SObject>> byId = new Map<Id, List<SObject>>();
        for (SObject record : records)
        {
            Id key = (Id)record.get(field);
            if (!byId.containsKey(key)) byId.put(key, new List<SObject>());
            byId.get(key).add(note);
        }
        return byId;
    }
}

Service Class

Class Declaration

You will need a service class for Notes. I usually pluralize Services in the class name, because the way I see it you will do many different things in most service classes you write.

public with sharing class NoteServices
{
    // methods
}

Consolidating Note Bodies

You need to compress these Note records into one String. It is very important to use List<String> because you will want to preserve order, and de-duplication is not particularly helpful (or likely to make an impact).

public static String consolidateBodies(List<Note> notes)
{
    if (notes == null) return '';
    List<String> bodies = new List<String>();
    for (Note note : notes) bodies.add(note.Body);
    return String.join(bodies, '\n');
}

Getting Parent Notes

Once you know that you can get a List<Note> for a specific parent, you can pass in these grouped records to the above method to map this String by the parent's Id.

public static Map<Id, String> consolidateBodies(List<Note> notes)
{
    final SObjectField PARENT = Note.ParentId
    Map<Id, List<Note>> parentToNotes = GroupBy.ids(PARENT, [
        SELECT Body FROM Note
        WHERE ParentId IN :Pluck.ids(PARENT, notes);
        ORDER BY Id DESC // if you want newest first
    ]);
    Map<Id, String> bodies = new Map<Id, String>();
    for (Id parentId : parentIds)
        bodies.put(parentId, consolidateBodies(parentToNotes.get(parentId)));
    return bodies;
}

Using Note Bodies

You now have everything you need. Just putting it together, build up a list of parent records with the Notes__c field populated. When you update these records, make sure you handle DmlException specifically. Typically from a trigger I would simply map the errors back to the source record (in this case a child record). I have an example of how to do that in a different answer.

public static List<SObject> syncParents(List<Note> notes)
{
    Map<Id, String> bodies = consolidateBodies(notes);
    List<SObject> parents = new List<SObject>();
    for (Id parentId : parentIds)
    {
        SObject parent= parentId.getSObjectType().newSObject();
        parent.put('Notes__c', bodies.get(parentId));
        parent.Id = parentId;
        parents.add(parent);
    }
    try { update parents; }
    catch (DmlException dmx) { /*determine proper error handling*/ }
}
Related Topic