12

Am working on a project in an org where I have to work with Dave Scott's Tidy, Streamlined, Bulkified Trigger Pattern also known as the Trigger Factory pattern which can be found in the Salesforce Cookbook. There are nuances to it that can make it difficult to work with for some use cases.

For example, one can't perform queries from within the Object Handler Class. That makes it difficult to use when one needs to query related records as they must be done in another helper class. Making those callouts appears to be something of a challenge as well. The "bulk before" and "bulk after" methods are iterators. As such, they're controlled by the for loop iterators in the Execute method of the Trigger Factory base class as per the code example below:

private static void execute(ITrigger handler)
{
    // Before Trigger
    if (Trigger.isBefore)
    {
        // Call the bulk before to handle any caching of data and enable bulkification
        handler.bulkBefore();

        // "before" code section omitted

    }
    else
    {
        // Call the bulk after to handle any caching of data and enable bulkification
        handler.bulkAfter();

        // Iterate through the records deleted passing them to the handler.
        if (Trigger.isDelete)
        {
            for (SObject so : Trigger.old)
            {
                handler.afterDelete(so);
            }
        }
        // Iterate through the records inserted passing them to the handler.
        else if (Trigger.isInsert)
        {
            for (SObject so : Trigger.new)
            {
                handler.afterInsert(so);
            }
        }
        // Iterate through the records updated passing them to the handler.
        else if (Trigger.isUpdate)
        {
            for (SObject so : Trigger.old)
            {
                handler.afterUpdate(so, Trigger.newMap.get(so.Id));
            }
        }
    }

    // Perform any post processing
    handler.andFinally();
}

What the above means is that when you get to the point of writing code for the actual trigger handler, in an After Trigger, it begins to look something like this:

// note: public variables defined earlier
public void afterInsert(SObject so)
{        
    User u = (User)So;
    if(u.ProfileId == ProfileId && u.isActive == true){
        CommUsrId2CtcId3.put(u.Id,u.contactId);

            // can't make a callout here from a list 
            // not certain if I can make a callout here at all on a per record basis
            // appears I need to wait until the finally method to do DML or callouts
            // can't set flag on custom field in user object for tracking
            // can't set a boolean flag to prevent re-entry to this part here since
            // it would be in an iterative loop & repeat up to 200 times
    }        
}

public void afterUpdate(SObject oldSo, SObject newSo)
{

}

public void andFinally()
{
    if(Trigger.IsAfter && Trigger.IsInsert){ 
        ApexSharing_UserHandler(CommUsrId2CtcId3, true);

        // could do DML to custom object which causes @future trigger to fire
        // could do DML on User object to set flag on custom field for tracking
        // could set a boolean flag to prevent re-entry
        // appears I can use a for loop here, but not certain of that

    }    
}

I'm trying to refactor code to work with this pattern and am feeling hamstrung by it, but the client insists that it be used. I can respect that, but need to be able to call other handlers to do the work and need to figure out where and how to call them in an efficient manner. Have I drawn the correct conclusions in my commented code about what I can and cannot do using this pattern? Are there other limitations I need to be aware of?

14
  • 2
    For what it's worth, that code would throw up all kinds of non-bulkified errors in the code scanner.
    – sfdcfox
    Commented Dec 29, 2015 at 15:36
  • 1
    i have much code written over the years in orgs that use this pattern and all my DML or future work is done in andFinally. You collect related -to- the-trigger-set objects in the bulkBefore or bulkAfter handlers and then in the before/after insert/update handlers, you build up maps of interesting things to do when you hit the andFinally. It is just a different way of thinking
    – cropredy
    Commented Dec 29, 2015 at 16:11
  • @sfdcfox Could you elaborate please? Just fixed a couple typos where copy/pasted from wrong sections. As I said, am trying to work with this framework and am not comfortable with it. Your comment isn't very helpful without providing at least one example.
    – crmprogdev
    Commented Dec 29, 2015 at 16:12
  • Thanks @crop1645. That's what I was concluding and trying to convey with my comments, but wanted to obtain some confirmation. Am I on target with respect to calling other classes that they also need to be done from the andFinally method?
    – crmprogdev
    Commented Dec 29, 2015 at 16:14
  • 1
    Crmprogdev, simple way of implementing the whole trigger framework is by not have any business logic inside the trigger but have them in apex classes. Then use conditions to determine which class to be called based on the event types. That way you will have more control and easy maintainable scenario.
    – DJD
    Commented Dec 29, 2015 at 16:27

2 Answers 2

6

The Tidy Trigger pattern requires one to think in a particular way

  1. Only one triggerhandler per SObject
  2. In the bulk before/after methods, collect up all related records that the rest of the trigger handler will need. For example, if the Trigger is on Opportunity, collect into a map all of the Opportunities' Accounts and perhaps a separate map, all of the Opportunities' OLI. I tend to do this in a separate OpportunityTriggerGateway class.
  3. In the before/after insert/update handlers, you process only a single record at a time but you can refer to the related records' maps in #2. As you process each record, you can save interesting things in collection variables that are part of the triggerhandler object. For example, for those Oppos in the trigger set that are closed won, perhaps you want to note them in a map of oIdToContractMap where you build up a Contract record from the Opportunity. You'll use this map in andFinally.
  4. In the andFinally (and typically, the execution of this after the after handlers, you will do bulk DML on related objects (in my example, insert Contracts). You can also launch async transactions. Order-dependent actions are handled here.

As @Giene noted, the handlers for the before/after insert/update/delete events typically invoke another class that does the actual work on an instance of the SObject (with an argument passed to the wherever you collected the related records in #1). These could be service-type classes or simply a wrapper on the SObject with utility methods.

Maintaining via a separate class a static collection of IDs that you don't want to reprocess on any sort of trigger recursion isn't represented in the pattern as documented but easy to add in.

When doing bulk DML in the andFinally, if you are supporting partial successes, you'll need to correlate the error on the DML'd record(s) to the triggered item that led to that DML so you can put addError on the sole triggered record and not fail the whole set of up to 200.

8
  • If one needs to query for say a recordTypeId to process records of interest, would that be done in the andFinally or could you do it in say the BulkAfter or BulkBefore section (depending on context) if you wanted to filter records in AfterInsert and/or AfterUpdate methods? Or, does one typically call another class to perform all "setup" queries they might need? As you say, one has to think in a very specific manner when constructing a trigger using this pattern. I'm attempting to learn what that is and how to do it. LOL!
    – crmprogdev
    Commented Dec 29, 2015 at 19:25
  • well, recordTypeId by name can be ascertained from Apex Describe so no SOQL is required. That said, the bulk handlers typically will need to fetch all related records that any of the record-by-record before/after insert/update/delete handlers could need. Say, you needed to lookup values in the triggered Oppo's Account to decide if an Oppo record requires an addError. Every Oppo's Account has to be prefetched in the bulk handler. andFinally can also do SOQL if the required records aren't needed by the record-by-record before/after insert/update handlers.
    – cropredy
    Commented Dec 29, 2015 at 19:32
  • Okay, recordTypeId, was a poor example. LOL. Point was that I might need to query a single value to assign to a public variable for repeated use as a filter. It sounds like the place to query though is in andFinally. Was trying to ascertain if one could also do a simple one at the start of either the BulkBefore or After methods since there's no iteration done at that point. From your comments, I'm hearing that using pre-fetch is the practice I should follow if I query anyplace other than in andFinally. Very helpful.
    – crmprogdev
    Commented Dec 29, 2015 at 19:45
  • Correct -- although there is always the static variable approach where you query for it when you first need it and retain for all subsequent record-by-record handlers. I tend to do this if I need to check something on some system setup object (User, DatedConversionRate, ...) where I want all triggers to not have to re-fetch this data that can be considered 'constant' for the life of the transaction. That is, I don't use the bulk before/after handlers to query setup objects. As an aside, the next "fresh" org I get to do I'll be looking at the Fawcett pattern
    – cropredy
    Commented Dec 29, 2015 at 20:23
  • 1
    yes - read this: developer.salesforce.com/forums/?id=906F000000091mJIAQ
    – cropredy
    Commented Dec 29, 2015 at 22:25
1

Simple way of implementing the whole trigger framework is by not have any business logic inside the trigger but have them in apex classes. Then use conditions to determine which class to be called based on the event types. That way you will have more control and easy maintainable scenario.

You must log in to answer this question.

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