Skip to main content
Content Starts Here
This is a publicly shared Knowledge Article from the Power of Us Hub - an online community for nonprofit and higher ed Salesforce users. Join the Hub.
Product Documentation

Deploy a Custom Apex Class in the TDTM Framework for HEDA

The Higher Education Data Architecture (HEDA) relies heavily on Apex to implement much of its functionality. Salesforce.org products use one trigger per object and Table-Driven Trigger Management (TDTM) to control the execution of Apex classes. You should familiarize yourself with the TDTM architecture if you’re developing your own custom Apex code, or integrating an external system with Salesforce. You don’t need to create new triggers for common standard objects such as Contacts and Accounts, or for HEDA custom objects. TDTM is extensible. When you write your own code, you only need to write Apex classes within the TDTM framework. You can create custom triggers and classes that work within the TDTM framework for your custom objects as well.

Note
If you need a refresher on the fundamentals of TDTM, read Table-Driven Trigger Management Overview.

Summary of Steps

Here’s an overview of the steps to deploy your custom Apex class in the TDTM framework:

  1. Create your Apex class. The class must:
    1. Use Global, not Public access modifier.
    2. Extend the hed.TDTM_Runnable class.
    3. Override the TDTM_Runnable run method, which returns a hed.TDTM_Runnable.DmlWrapper and takes the following parameters:
      1. List<SObject> newlist
      2. List<SObject> oldlist
      3. hed.TDTM_Runnable.Action triggerAction
      4. Schema.DescribeSObjectResult objResult
    4. Use DMLWrapper for efficient DML operations.
  2. Write your test class.
  3. Add a Trigger Handler record with the appropriate field data.
Tip
As a best practice, use _TDTM as the suffix for your class (example: OPP_MyAwesomeClass_TDTM.cls)

Technical Overview

In TDTM design, our triggers call the TDTM_TriggerHandler class and pass it all the environment information. The actual business logic that needs to run when an action occurs on a record is stored in plain old classes. We created a custom object, Trigger_Handler__c, to store which classes should run for each object, along with the related actions. In this object, we also define whether the class is active or inactive, what order it should execute in for the same object, and other settings. The Trigger Handler then calls these classes when appropriate, which provides the advantage of running all the DML operations at the end of execution through the DmlWrapper class. For more information about the Trigger Handler object, read Manage Trigger Handlers.

Note
Our codebase is open source. Explore the Trigger Handler class and see how our package code implements the TDTM framework in our GitHub repository. HEDA is referred to as HEDAP in Git. HEDAP is a public repository and you can contribute code if you wish.

Not every Salesforce object has a TDTM trigger. Here is the list of objects that have a TDTM trigger in the HEDA package:

  • Account
  • Address
  • Affiliation
  • Campaign
  • Campaign Member
  • Contact
  • Course
  • Course Enrollment
  • Course Offering
  • Opportunity
  • Program Enrollment
  • Relationship
  • Term
  • Trigger Handler

If you create your own custom objects, or want to run a TDTM class on a standard object that doesn’t have a trigger provided by the package, you can create a trigger within the TDTM framework. Use the TDTM_Global_API global class and reference the hed namespace. Here’s an example code snippet:

trigger TDTM_MyCustomObject on MyCustomObject__c (after delete, after insert, after undelete,after update, before delete, before insert, before update) {

hed.TDTM_Global_API.run(Trigger.isBefore, Trigger.isAfter, Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete, Trigger.isUndelete, Trigger.new, Trigger.old, Schema.SObjectType.MyCustomObject__c);
}

Create an Apex Class

Here’s an example of a custom class that follows the TDTM design:

// Trigger Handler Class on CampaignMember, to reset any Contact's HasOptedOutOfEmail field

global class CM_ClearEmailOptOut_TDTM extends hed.TDTM_Runnable {

   // the Trigger Handler’s Run method we must provide
   global override hed.TDTM_Runnable.DmlWrapper run(List<SObject> newlist, List<SObject> oldlist,
       hed.TDTM_Runnable.Action triggerAction, Schema.DescribeSObjectResult objResult) {

       hed.TDTM_Runnable.dmlWrapper dmlWrapper = new hed.TDTM_Runnable.DmlWrapper();

       if (triggerAction == hed.TDTM_Runnable.Action.AfterInsert) {
           list<ID> listConId = new list<ID>();
          
           for (CampaignMember cm : (list<CampaignMember>)newlist) {
               if (cm.ContactId != null) {
                   listConId.add(cm.ContactId);
               }
           }
           list<Contact> listCon = [select Id, HasOptedOutOfEmail from Contact where Id in :listConId];
           for (Contact con : listCon) {
               con.HasOptedOutOfEmail = false;
           }
           dmlWrapper.objectsToUpdate.addAll((list<SObject>)listCon);
       }
       return dmlWrapper;
   }
}

Note that because the class is external, you need to declare the class and its method global, and use the hed prefix when calling classes inside the HEDA package. If you use the public identifier instead, you won’t get an error, but you won’t see the expected behavior either. It will appear as if the class doesn’t exist or is inactive.

Additionally, if the class you are writing is inside another managed package, include the package prefix when entering the class name in the Class__c field of the Trigger Handler record. In our example, if CM_ClearEmailOptOut_TDTM was inside a managed package with prefix foo, its name should be entered as foo.CM_ClearEmailOptOut_TDTM.

Create a Test Class

  1. By default, test classes can’t see data in your org. Information about your Trigger Handler record resides in the Trigger Handler object and as a result, you must load the default Trigger Handlers into memory using the TDTM_Global_API class and getTdtmToken method.
    List<hed__TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();

    Read more about this globally exposed class and its methods at HEDA Codebase Documentation.

  2. Add information about your Trigger Handler. For example:
    tokens.add(new hed.TDTM_Global_API.TdtmToken('MyAccAwesomeClass_TDTM', 'Account', 'AfterInsert;AfterUpdate;AfterDelete', 2.00));
    			
  3. Pass your Trigger Handler configuration as a parameter of the setTdtmConfig method:
    hed.TDTM_Global_API.setTdtmConfig(tokens);
  4. Insert your data and test your Trigger Handler:
    // setup our test data…
    test.startTest();
    // do some operations…
    test.stopTest();
    // validate our results...

Here’s an example of a test class that follows the TDTM design:

@isTest
public class MyTriggerHandler_TEST {

   @isTest
   static void testCMTrigger() {

       // first retrieve default HEDA trigger handlers
       List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();

       // Create our trigger handler using the constructor
    tokens.add(new hed.TDTM_Global_API.TdtmToken('MyAccAwesomeClass_TDTM', 'Account', 'AfterInsert;AfterUpdate;AfterDelete', 2.00));

       // Pass trigger handler config to set method for this test run
      hed.TDTM_Global_API.setTdtmConfig(tokens);

       // setup our test data...
       test.startTest();
       // do some operations...
       test.stopTest();
       // validate our results...
   }
}

Create a Trigger Handler Record

When you create a custom class of your own, you must also create a Trigger_Handler__c record that references the class. Read Manage Trigger Handlers for more information.

There must be a Trigger_Handler__c record for each class managed by TDTM. Take a look at the Trigger Handler tab to see the list of Trigger_Handler__c records in your org.

Trigger Handler List View

Load Order is important in TDTM. If you want your code to fire after HEDA packaged code, choose a number after the last number used for your object. For example, you could use 2 for a custom Account class. You can also use decimals if you want your code to run between two other classes. For example, you could use 2.5 for a custom Contact class.