12

I'm working on a major project that has numerous triggers creating new records on 4 objects while also syncing updates back and forth between 3 of the 4 objects. The new records are primarily created when a trigger fires on only 1 object, but there's a total of 6 objects involved with a lot of pieces of data moving around between the objects.

This seemed like the ideal project to implement Appleman's Trigger Class Architecture. Unfortunately, I'm having a great deal of difficulty making it all work together. Apparently I don't fully understand how to direct flow between classes either prior or following "in process" DML operations; as in ones that happen while the trigger is executing, but not completely finished and has additional work to do.

I'm going to attempt to post some code below of class or two to illustrate what I'm working with for discussion.

First, here's the very simple trigger code:

trigger InsrtSyncCE_Event_On_Opp on Opportunity (after insert, after update) {

   TriggerDispatchMain1.Entry1('Opportunity',trigger.IsBefore, trigger.IsDelete, trigger.IsAfter, trigger.IsInsert, trigger.IsUpdate, trigger.IsExecuting, trigger.new, trigger.newmap, trigger.old, trigger.oldmap);
}

Next, here's the Main "Trigger Class" that "Dispatches" the work to other classes:

public class TriggerDispatchMain1 {

/* This class dispatches triggers to other classes for execution */

public static list<Opportunity> opstoupdate = new list<Opportunity>();
public static list<Event> evtstoupdate = new list<Event>();
public static Boolean InClass1;

public interface ITriggerEntry
{
    void MainEntry(String TriggerObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> newlist, map<ID, SObject> newmap, list<SObject> oldlist, map<ID,SObject> oldmap);
    void InProgressEntry(String TriggerObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> newlist, map<ID, SObject> newmap, List<SObject> oldlist, map<ID,SObject> oldmap );
}

public static ITriggerEntry activefunction = null;

public static void Entry1(String TriggerObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, List<SObject> newlist, Map<ID, SObject> newmap, List<SObject> oldlist, Map<ID,SObject> oldmap)
{   
    if(activefunction != null) 
    {
        activefunction.InProgressEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap );
        return;
    }

    if(TriggerObject == 'Opportunity') {

        if(IsAfter==true){ 

            if(ConditionA){ 

                activefunction = new TriggerMain1Class1();
                activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);

            /* The above class creates a new Event (DML) for each Opportunity record  */ 
            /* then resets the Opportunity field that signalled condition A (DML) */ 

            }else{

                activefunction = new TriggerMain1Class2();
                activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);

            /* Above class updates the Event when the Opportunity Owner changes */                      
            }
        }// end if(IsAfter == true
    }// end if(TriggerObject == 'Opportunity' 

    if(TriggerObject == 'Event') {

        if(IsAfter==true){

            list<Event>evtnewlist = (list<Event>)newlist;
            map<Id,Event>evtnewmap, etc;

            if('Create Assignment' condition){
                activefunction = new TriggerMain1Class3();
                activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);
                /* this class requires Event to be updated after Assignment is created (DMLx2) */
            }
        }else{
        /* sync Event with assignment */

            activefunction = new TriggerMain1Class4();
            activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);
       }
    } // End if(TriggerObject == 'Event'

    if(TriggerObject == 'Assignments__c'){ 
     // more of the same
    } // End if(TriggerObject == 'Assignments__c'
}
}

Next, here's the structure for the actual classes that contain the working code normally contained in a trigger:

public class TriggerMain1Class1 implements TriggerDispatchMain1.ITriggerEntry {

/* This class creates Events from Opportunities */

public void MainEntry(String TriggerObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, List<SObject> newlist, Map<ID, SObject> newmap, List<SObject> oldlist, Map<ID,SObject> oldmap)
{
    DiagnosticsInstrumentation.Push('DiagnosticsTriggers1.MainEntry');
    AfterUpdateOpportunityInsertEvent((List<Opportunity>)newlist, (List<Opportunity>)oldlist, (Map<ID,Opportunity>) newmap, (Map<ID,Opportunity>) oldmap, IsAfter, IsInsert, IsUpdate);
    DiagnosticsInstrumentation.Pop();
}

public static void AfterUpdateOpportunityInsertEvent(List<Opportunity> newlist, List<Opportunity>oldlist, Map<ID, Opportunity> newmap, Map<ID, Opportunity> oldmap, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate)
{
        list<Opportunity> oppnewlist = (list<Opportunity>)newlist;
    list<Opportunity> oppoldlist = (list<Opportunity>)oldlist;
    map<Id, Opportunity> oppnewmap = (map<Id,Opportunity>)newmap;
    map<Id, Opportunity> oppoldmap = (map<Id,Opportunity>)oldmap;

     /* code to sort the trigger.new vs trigger.old */

      /* code to create the Event(s) */

     /* code to get/store Id's of Opps that need to have field reset */

     /* DML operation to insert the Events(s) */

     /* DML operation to update the Opportunities */

    } // End of Inner Class


public void InProgressEntry(String TriggerObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, List<SObject> newlist, Map<ID, SObject> newmap, List<SObject> oldlist, Map<ID,SObject> oldmap)
{
     /* It appears that code can be redirected here provided there's no DML */
     /* in the prior inner class. Otherwise, the DML seems to trigger a redirect */
     /* back to the TriggerDispatchMain1 Class preventing this code from executing */

    if(TriggerObject == 'Event' && IsAfter)
    {
        TriggerDispatchMain1.activefunction = new triggerMain1Class3();
        TriggerDispatchMain1.activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap);
        TriggerDispatchMain1.activefunction = this; 
    }

}//End inner class

} // End Outer Class

In essence, I don't seem to be able to control where I'd like the code to go or prevent multiple instances of triggers from firing due to the extra DML operations (or workflow and validation kicking in each time for that matter). As an example, I'd ideally like to be able to send the code when fired on Opportunity to create a new Event, directly into creating a new Assignment__c after it's finished without going through the trigger that fires on Event. The original Execution context of the 1st trigger that fired on Opportunity is still in effect when that happens.

I also want to use public static boolean InClass1 2, 3, etc variables to prevent recursion in the sync portions of the code, yet I'm not entirely certain how well that will work with this architecture since it operates on "instances" and the static variables won't act like instance variables that stay within the context of the same instance of a class. Instead, from what I've read and am seeing in testing, they'll act more like "Global" variables and won't expire when the instance does. If I understand things correctly (big "if"), if I put static variables in the sub class when it's called, it appears I lose the ability to prevent a new instance of a trigger from entering the sub class from the Dispatcher class.

So, any of you with good knowledge of constructors and classes, I'd greatly appreciate your assistance right now. With not having a Java background, this is an area that I'm not very strong in and still trying very hard to understand.

Edit

First, I should mention that I accidently omitted a crucial part of TriggerMain1Class1; the inner class called public void MainEntry and had gone directly to the code that's inside another inner class called public static void AfterUpdateOpportunityInsertEvent. My apologies for that. I hope the code makes much more sense now that those omissions have been included.

I need to add that one of the things I'd really like help with is understanding how to create another class or constructor of some sort within the existing ones that would allow me to redirect DML operations either back to the Dispatcher class or to another class that's dedicated to DML operations. I'm thinking that might help control flow by consolidating DML operations and preventing additional instances of triggers firing when "cleanup" DML operation are done after the primary task of the original code has completed (resetting values in Opportunity or Event fields that were used to tell the code to create new Events or Assignments__c's, etc).

The fact that the TriggerMainClass1 implements TriggerDispatchMain1.ITriggerEntry is something I don't quite know how to work around (or with) in terms of creating additional constructors other than the method used in Appleman's book using the code inside the public void InProgressEntry I've shown. Can I for example use the same code from within the public static void AfterUpdateOpportunityInsertEvent inner class of TriggerMain1Class1?

It makes sense that I could do that, however, I believe it works (please correct me if I'm mistaken) by sending things back through the Dispatcher class and then to the class that's being called. If I'm sending it back there, it would seem to me that I may as well use that class to do the DML by copying the updates into a static variable. Doing that would bring up the issues I raised earlier about handling static variables. Otherwise, a constructor carrying the objects to update in a map (or simply the Id's in a set) would be helpful; another issue I'm not confident on how to handle. I'd want a second constructor that has an additional method with the map containing the Ids and the object to be updated. Would I simply need to duplicate the ITrigger Constructor and add the map to the end of it when creating the constructor?

Does anyone have any thoughts or suggestions on how to implement the concepts I'm speaking of within this framework/architecture? I really need to get this working ASAP. I've been going in circles for well over a week now (going on two) and need to move things forward to make some real progress.

Update

I've done some additional work trying to create additional implementation constructors with limited success and need some help fine-tuning them to make them work. I know that a class can implement more than one method which is the approach I'm trying to set up. What I want to do is configure a class to be able to return DML for processing either to the Dispatcher class or another separate DML class which should cause the instance of the current trigger to expire. Here's some sample code of what I've tried or am attempting. One thing which is of concern is the static variable activefunction not applying to any of the other interfaces. I don't know how to work around that except to create additional static variables.

Here's snippets of the additional code:

public class TriggerDispatchMain1 {

public interface PDmlOpsEntry
{
    void MainEntry(String sObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> toupdate);
    void InProgressEntry(String sObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> toupdate);
}

/* note that other than the booleans, I've removed all the trigger contexts, */
/* only leaving the "toupdate" list for direct DML processing */
/* The booleans were important to keep in case the DML happened to be */ 
/* an insert or delete and not just an update */ 
/* Since I expect these to primarily be "returned", having an InProgressEntry */
/* context seemed like an essential thing to have */

public static PDmlOpsEntry activefunction1 = null;

public interface ITriggerEntry1
{
    void MainEntry(String sObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> newlist, map<ID, SObject> newmap, list<SObject> oldlist, map<ID,SObject> oldmap, list<SObject> toupdate);
    void InProgressEntry(String sObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> newlist, map<ID, SObject> newmap, List<SObject> oldlist, map<ID,SObject> oldmap, list<SObject> toupdate);
}
/* I also attempted adding a 2nd ITriggerEntry, but it didn't seem to work out well */
/* perhaps the problem was in the implementation by not using the "this" keyword? */

public static void Entry2(String sObject, Boolean IsBefore, Boolean IsDelete, Boolean IsAfter, Boolean IsInsert, Boolean IsUpdate, Boolean IsExecuting, list<SObject> toupdate)
{

    if(activefunction == true && activefunction1 != true) 
    {
        activefunction1.InProgressEntry(String, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap );
        return;
    }
    /* DML ops must enter as activefunction and initiate an activefunction1 */

        // Determine 
}

Here's how I've attempted to put the above into practice with the dependent classes:

public class TriggerMain1Class1 implements TriggerDispatchMain1.ITriggerEntry,  TriggerDispatchMain1.ITriggerEntry1, TriggerDispatchMain1.PDmlOpsEntry {

/* each TriggerMain1Class# has then needed an additional entry constructor for each */
/* interface and an additional method for each one before it could be compiled, */
/* assuming I was able to compile it at all */

I was hoping to be able to call something like the following from the dependent classes:

toupdate = (list<Opportunity>)oppstoupdate;
TriggerDispatchMain1.activefunction = new triggerDispatchMain1.PDmlOpsEntry();
        TriggerDispatchMain1.activefunction.PDmlOpsEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap, toupdate);
        TriggerDispatchMain1.activefunction = ('Opportunity', ToUpdate==true, toupdate);

Any thoughts on how to fine tune this so it will work from those of you with any knowledge of constructors and interfaces? I'm thinking that I could be over-thinking this and don't need to carry forward the 2nd interface to subclasses. In fact, I'm thinking I could possibly send all of the DML to a separate class entirely using this kind of method or similar. I just don't have the Java background to know exactly how to do the constructs.

I had also thought about adding a separate static variable for each type of sObject to the Dispatcher class that would look like this:

public static list<Opportunity> opstoupdate = new list<Opportunity>();

I could call them from any of the dependent classes, clear them, call an instance of the DML class, retrieve the "toupdate" static variable for that object and do the DML. My concern would be losing data between instances of the class that would call it (the variable is static and not an instance) if I took that approach. Any thoughts on a work-around to that? Any and all comments are welcome. I feel as though I'm writing a blog here.

  • 1
    Could you provide more information of what your workflow/use case is? The volume of code snippets has made answering what you are looking for more prohibitive. Also, have you downloaded Dan's code from advancedapex.com/samplecode as this may allow you to use a template rather than completely roll your own for now. – pbattisson Jul 28 '13 at 16:42
  • I have all of his code and that's what I started with Pat. That's the frustrating part. The use case is taking Opportunities, creating events (assignments), mirroring them out through a communities portal so they're visible to contract employees as Assignments__c. They "bid" on them through another object. Assignments are linked to accounts, contacts, etc for payments & reports once assigned, then synced back & forth as necessary. The design is for triggers to move all the data back & forth! – crmprogdev Jul 28 '13 at 17:56
  • Some of the triggers already exist and work fine by themselves. When I've tried moving them to this architecture, the multiple DML ops within them seem to be breaking the flow. – crmprogdev Jul 28 '13 at 18:00
7

This recent blog post by Hari Krishnan might be helpful. He's taken the trigger architecture and used it as a foundation in his framework. It's downloadable and might help to more cleanly separate concerns for you.

But as pbattisson mentioned, the volume of code you posted makes determining the issue difficult. I'd suggest starting fresh with new bare-bones triggers/classes that only solve the specific issue you're having with extra code execution (DML etc.) Or install Hari's unmanaged app in your org and see if it solves the problem.

Also I noticed you're doing an after insert / after update. If you plan on changing Opportunity records you might consider doing this on before update/insert and avoid the extra DML altogether.

Good luck!

  • Thanks Adam, I'll look at the blog & unmanaged app. Originally, I did remove what I didn't need of Appleman's code, then slowly added some back in (diagnostics mostly). Unfortunately doing before update or insert isn't an option. Majority of the 2nd after-inserts/after-updates are syncing back the URL cross-links(Id's) once initial records have been created which can't be done until I have the Id's. The rest is resetting booleans that tell us to create the records. All others are genuine updates that need synced. – crmprogdev Jul 28 '13 at 18:49
8

@Adam appears to have led me to what I hope will make what I've got going work. If I can't salvage what I've got (time is of the essence), Hari Krishnan's trigger architecture will definitely be the way for me to go!

It builds on the framework that Appleman outlined and readily says in his blog post A Most Interesting Apex Trigger Framework "the examples I used in the book do not, in fact, accurately reflect the framework I used in our own products. The framework we use is considerably more sophisticated. But the examples do reflect the concepts that our framework uses." I knew going in that I would need to do some additional work to make his code fit my application. I just didn't realize how challenging that was going to be.

What I found in Hari's code was that he uses a separate class to define his interface which is an elegant solution to the problem I've been having and was trying to solve:

public abstract class TriggerHandlerBase implements ITriggerHandler {
    protected Map<Id, SObject> sObjectsToUpdate = new Map<Id, SObject>();

    /** 
    * @author Hari Krishnan
    * @date 07/16/2013

    public abstract void mainEntry(TriggerParameters tp); // trigger param same as Appleman's

    public virtual void inProgressEntry(TriggerParameters tp) {}

    public virtual void updateObjects() { // The part I've especially needed!!!
    if(sObjectsToUpdate.size() > 0)
        update sObjectsToUpdate.values();
    }
}

He also uses classes than extend a base class to "build-up" the functionality he desired. This is something I was trying to avoid for reasons of complexity, but its obviously something he found necessary.

My apologies for such a lengthy thread, but I hope others will find it useful since I searched extensively before posting and couldn't seem to find any resources that referenced Appleman's trigger class code. I also didn't know how to convey what I was attempting without posting at least some of the essential code for the classes along with psuedocode.

I'm merely a subcontractor on this particular project and have invested a huge amount of my own time trying to make this work in the hope that when finished, I'd have something to use as a quick starting point for other projects in the future. Has it been worth it? Financially, no. However, I've learned a great deal, so in that sense, Yes. I can deal with an architect who's VERY unhappy that I'm way behind schedule...(sigh)

4

At the end of the day, it's important that your requirements drive the architecture (rather than the other way around). The trigger architecture in the book is intended to provide the concepts - not a solution for every application.

That said - there are a couple of ways to approach your problem (as I understand it). Hari's blog post may indeed be the right solution for you. Another approach is to think of things in terms of sequences of operations. Let's say you have functional blocks A, B, C and D. Trigger 1 needs to perform operations ABD. Trigger 2 needs to perform operations BCD.

When trigger 1 occurs, you could instantiate the handler classes for A, B and D, and call the MainEntry function for each one in order. Alternatively you could call the MainEntry function for A and have it instantiate B and D and call them in order. The advantage of the latter approach is that you don't have to use the MainEntry function on B and D - you can use another class method that allows passing/tracking of objects to update.

In effect, Trigger 1 starts Sequence "A" - where the sequence ABD is controlled by the A handler, which is also responsible for DML updates at the end of that sequence of operations. A similar approach can be used by Trigger 2 and the sequence that starts with B.

The key thing about this approach is that the InProgressEntry function isn't used at all. It's real value is as a mechanism to insure that you are ignoring all triggers during the entire ABD sequence - and that includes those caused by workflows.

But again, that's just one way to approach the problem. I've used that approach extensively. I've also used static variables, which may seem incompatible with the instance nature of the framework, but really aren't - because you're dealing with a single thread and a single execution context. If your scenarios are really complex, some form of state machine may be in order.

Hope that helps.

  • Excellent observations. I had hoped to have the initial trigger on Opp fire, create the Event, then directly create the Assignment all in the same execution context. Doing that also requires an update operation to both Opp and Event. Before starting this project, 4 triggers already existed on Opportunity. There's also a LOT of workflow. Its a very complex org that's been piecemealed together. I also expect to see several more feature requests added to the project involving other objects before it's finished. 3 other objects are already involved I've not mentioned. – crmprogdev Jul 29 '13 at 18:57
  • I hear you. These architectures are best when starting out and on packages, but in the real world you often end up using them for just a part of the code, because it's too costly or complex to rewrite/refactor everything. – kibitzer Jul 29 '13 at 21:59
  • @kibitzer: I am going through your thought provoking book and finding it helpful to expand my knowledge. However, I have a very very basic question regarding the trigger pattern which was discussed in book. In the dispatcher class, we keep track of activeFuntion obj. If null , the call the inProgress mehtod. Now, I believe this dispatcher would run twice, once before trigger whereas one after trigger. So in after Trigger , the object would always be populated (if I do not initialize the object only on After trigger) and would go into InProgress. But is is not reentrant due to DML. – Deep Singhal Oct 15 '17 at 20:40
  • Actually, you call inProgress if the activeFunction is NOT null. The purpose of this is to direct any DML operations caused by the initial trigger to the inProgress methods. Once the initial trigger is complete, activeFunction will be set to null again. This entire process repeats for both before and after triggers, so indeed, you can dispatch for both. – kibitzer Oct 16 '17 at 21:25
  • Cool.. thanks @kibitzer. Got the idea. Was missing resetting the object to Null after calling the mainEntry. – Deep Singhal Oct 21 '17 at 5:48

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.