4

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);
            }
        }
    }
4

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*/ }
}
| improve this answer | |
  • Very elegant. Am pleased you took my 'bait'. Please "teach us to fish" by adding comments to explain why/how you chose your methods and what they do. Most won't understand or follow your code without further explanation. As you know, using code one doesn't understand can be dangerous, difficult to modify & impossible to edit to implement for similar use cases. Thank you for your contribution. Always love seeing the clean code you write. – crmprogdev Dec 20 '15 at 13:00
  • @crmprogdev You are right about that and I harp on people for it all the time. I ran out of attention span and hoped the titles would help. I'll fill in with detail though. – Adrian Larson Dec 20 '15 at 15:44
  • +1 for a Class named Pluck as in 'to pick out' although I like the alternate definition suddenly remove someone from a dangerous or unpleasant situation -- something that could be said for many code samples we see elsewhere on SFSE. – cropredy Dec 20 '15 at 18:37
  • Thank you, well done! Now I can go fishing with a different parent and their children using my own gear. ;) – crmprogdev Dec 20 '15 at 20:00
2

I'm confident there are many different ways of doing this. This is just one way I can think of to help get you started. Modify it however it suits you. I'm certain that once I post this, someone else will come along with a really slick method that's much nicer. ;)

In your helper class, you'll likely want to do something like this or similar.

First, for the objects you know you want the class to cover, do the following describe call:

// sObject types to describe
String[] types = new String[]{'grc__Incident__c','other_sObjectName_1', 'other_sObjectName_2', 
'other_sObjectName_3', 'other_sObjectName_4'};

Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types);

// here's some of what you can do with those results:

for(Schema.DescribeSobjectResult res : results) {
    System.debug('sObject Label: ' + res.getLabel());
    System.debug('Number of fields: ' + res.fields.getMap().size());
    System.debug(res.isCustom() ? 'This is a custom object.' : 'This is a standard object.');
    // Get child relationships
    Schema.ChildRelationship[] rels = res.getChildRelationships();
    if (rels.size() > 0) {
        System.debug(res.getName() + ' has ' + rels.size() + ' child relationships.');
    }
}

What you really want to do in the above is add the appropriate code to get the 'Notes__c' field. See the Apex docs on Dynamic Apex and Describe FieldResults Methods. Pay attention to the getName() method in particular to read about some of what you need to learn. That's the one that will help you find 'Notes__c'.

// from your `List<Note> notes`, you're going to need to take the set from this line: 
incid.add(n.ParentId); 
//and do something like:

string objstr = '';
string objstrg = '';
map<Id,string>NoteId2SobTypeMap = new map<Id,string>();

for:(Id tId : incid){

    Schema.SObjectType t = tId.getSobjectType();

    for(Schema.DescribeSobjectResult r:results){

        if(r.getSobjectType() == t){
           NoteId2SobTypeMap.put(tId,r); 

           if(!objstr.contains(string.valueOf(r))){
               objstrng = objstr +','+ string.valueOf(r);
               obstr = objstrng;
           }

           // strings are immutable, so can't always add them to themselves
           // thus use of intermediate object 
    }

}

// now you can assemble your dynamic query containing objects in objstr and proceed from there

// can then call your method on the field once you have the results of query

This should give you a pretty good basis for creating your code.

Note: This code is entirely written "on the fly". It's completely untested and may have syntax errors. It's intended to convey the principles to help get you started in writing your own code.

| improve this answer | |
  • 1
    worth looking at is using Andy Fawcett's DLRS tool which apparently can rollup Notes, do concatenation in the Spring 15 release, and would not require any code at all. I've not personally tried this use case with DLRS but I definitely would give it a shot. This blog gives an example of rolling up Notes succeedwithsalesforce.com/… – cropredy Dec 20 '15 at 0:57
  • See, I KNEW if I posted an answer to this, folks would come crawling out of the woodwork with all kinds of alternatives. ;) ROTFL! Great spot my friend! – crmprogdev Dec 20 '15 at 1:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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