You are here:
Deploy a Custom Apex Class in the TDTM Framework for NPSP
The Nonprofit Success Pack (NPSP) 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, Accounts, and Opportunities or for NPSP custom objects. TDTM is extensible. When you write your own code, you only need to write Apex classes within the TDTM framework.
- Summary of Steps
Here's an overview of the steps to deploy your custom Apex class in the TDTM framework. - Technical Overview
Learn how NPSP triggers work and the logic behind them. - Create an Apex Class
Here's an example of a custom class that follows the TDTM design. - Create a Test Class
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 cached Trigger Handlers into memory. - 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. - Using the TDTM_RunnableMutable Interface
Custom implementations of TDTM can't interact with the TDTM_Runnable.DmlWrapper collection when it already has records in the objectsToUpdate collection. If you try to include your own DML in the objectsToUpdate collection, you'll get a Duplicate id in list error. While it's possible to run your code asynchronously or perform DML separately from theTDTM_TriggerHandler, consider using an alternative interface called TDTM_RunnableMutable.
Summary of Steps
Here's an overview of the steps to deploy your custom Apex class in the TDTM framework.
- Create your Apex class. The class must:
- Extend the
npsp.TDTM_Runnableclass. Depending on your implementation details, you may also use the TDTM_RunnableMutable interface. - Override the
TDTM_Runnablerun method, which returns anpsp.TDTM_Runnable.DmlWrapperand takes the following parameters:-
List<SObject> newlist -
List<SObject> oldlist -
npsp.TDTM_Runnable.Action triggerAction -
Schema.DescribeSObjectResult objResult
-
-
Use
DMLWrapperfor efficient DML operations.
- Extend the
- Write your test class.
- Add a Trigger Handler record with the appropriate field data .
_TDTM as the suffix for your class (example: OPP_MyAwesomeClass_TDTM.cls)Technical Overview
Learn how NPSP triggers work and the logic behind them.
In TDTM design, our triggers call the
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,
TDTM_TriggerHandler
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.
Not every Salesforce object has a TDTM trigger. You can see which objects have a TDTM trigger in Apex Class Descriptions for NPSP.
To see all Apex triggers present in your org, go to Apex Triggers in Setup.
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_Config_API global class and reference the
npsp 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) {
npsp.TDTM_Config_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.
global class CM_ContactRecentCampaign_TDTM extends npsp.TDTM_Runnable {
global override npsp.TDTM_Runnable.DmlWrapper run(List<SObject> newlist,
List<SObject> oldlist,
npsp.TDTM_Runnable.Action triggerAction,
Schema.DescribeSObjectResult objResult) {
npsp.TDTM_Runnable.dmlWrapper dmlWrapper = new npsp.TDTM_Runnable.DmlWrapper();
List<Contact> contactsToUpdate = new List<Contact>();
List<CampaignMember> newCMList = (List<CampaignMember>) newlist;
List<CampaignMember> oldCMList = (List<Campaignmember>) oldlist;
if (triggerAction == npsp.TDTM_Runnable.Action.AfterInsert) {
for (CampaignMember cm : newCMList) {
if (cm.Status == 'Responded') {
contactsToUpdate.add(new Contact(Id = cm.ContactId, Most_Recent_Responded_Campaign__c = cm.CampaignId));
}
}
}
else if (triggerAction == npsp.TDTM_Runnable.Action.AfterUpdate) {
for (Integer x = 0; x < newCMList.size(); x++) {
if (newCMList[x].Status == 'Responded' && (newCMList[x].Status <> oldCMList[x].Status) ) {
contactsToUpdate.add(new Contact(Id = newCMList[x].ContactId, Most_Recent_Responded_Campaign__c = newCMList[x].CampaignId));
}
}
}
dmlWrapper.objectsToUpdate.addAll((List<Contact>)contactsToUpdate);
return dmlWrapper;
}
}
Note that because the class is external you need to use the npsp prefix when calling classes inside the NPSP package. 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_ContactRecentCampaign_TDTM was inside a managed package with prefix foo, its name should be entered as foo.CM_ContactRecentCampaign_TDTM.
Create a Test Class
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 cached Trigger Handlers into memory.
- Use the global class
TDTM_Config_APIand its methodgetCachedRecords.List<npsp__Trigger_Handler__c> listHandlers = npsp.TDTM_Config_API.getCachedRecords(); - Add information about your Trigger Handler. For example:
npsp__Trigger_Handler__c th = new npsp__Trigger_Handler__c(); th.Name = 'MyOppAwesomeClass_TDTM'; th.npsp__Class__c = 'OPP_MyAwesomeClass_TDTM'; th.npsp__Object__c = 'Opportunity'; th.npsp__Trigger_Action__c = 'AfterInsert'; th.npsp__Active__c = true; th.npsp__Load_Order__c = 1; th.npsp__Asynchronous__c = false;Important You must declarenpsp__Asynchronous__cto befalseortruein your test class. A null value causes the test to fail. - Add your Trigger Handler to the cached records:
listHandlers.add(th); - 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 TDTM design:
@isTest
private class CM_ContactRecentCampaign_TEST {
static testMethod void test_CM_Responded_TDTM() {
// Retrieve default NPSP Trigger Handlers
List<npsp__Trigger_Handler__c> triggerHandlers = npsp.TDTM_Config_API.getCachedRecords();
// Add our Trigger Handler to cached Trigger Handlers
npsp__Trigger_Handler__c th = new npsp__Trigger_Handler__c();
th.Name = 'MyCMTriggerHandler';
th.npsp__Class__c = 'CM_ContactRecentCampaign_TDTM';
th.npsp__Object__c = 'CampaignMember';
th.npsp__Trigger_Action__c = 'AfterInsert;AfterUpdate;';
th.npsp__Active__c = true;
th.npsp__Load_Order__c = 1;
th.npsp__Asynchronous__c = false;
triggerHandlers.add(th);
// set up test data
Contact con1 = new Contact(FirstName = 'Jess', LastName = 'Lopez');
insert con1;
List<Campaign> campList = new List<Campaign>();
campList.add(new Campaign(Name = 'Test Campaign 1', IsActive = true));
campList.add(new Campaign(Name = 'Test Campaign 2', IsActive = true));
insert campList;
List<CampaignMember> campMemberList = new List<CampaignMember>();
campMemberList.add(new CampaignMember(ContactId = con1.Id, CampaignId = campList[0].Id, Status = 'Responded'));
campMemberList.add(new CampaignMember(ContactId = con1.Id, CampaignId = campList[1].Id, Status = 'Sent'));
insert campMemberList;
test.startTest();
// Test 1: Insert 'Responded' Campaign Member and verify Contact update.
con1 = [SELECT Id, Most_Recent_Responded_Campaign__c FROM Contact WHERE Id = :con1.Id];
System.assertEquals(con1.Most_Recent_Responded_Campaign__c, campList[0].Id);
// Test 2: Change Status from 'Sent' to 'Responded' and verify Contact update
campMemberList[1].Status = 'Responded';
update campMemberList[1];
con1 = [SELECT Id, Most_Recent_Responded_Campaign__c FROM Contact WHERE Id = :con1.Id];
System.assertEquals(con1.Most_Recent_Responded_Campaign__c, campList[1].Id);
test.stopTest();
}
}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 in NPSP for more information.
There must be a Trigger_Handler__c record for each class managed by
TDTM. Take a look at the Trigger Configuration page in to see the list of Trigger_Handler__c records in your
org.
Load Order is important in TDTM. If you want your code to fire after NPSP packaged code, choose a number after the last number used for the object. For example, you can use 3 for a custom Account class. Your class can also run in parallel with another class. For example, you can use 2 for a custom Contact class. However, you won't know for sure if your class or the packaged classes run first. If execution order precision is needed, it’s best to design your class to run after the packaged classes.
Using the TDTM_RunnableMutable Interface
Custom implementations of TDTM can't interact with the TDTM_Runnable.DmlWrapper collection when it already has records in the objectsToUpdate collection. If you try to include your own DML in the objectsToUpdate collection, you'll get a Duplicate id in list error. While it's possible to run your code asynchronously or perform DML separately from theTDTM_TriggerHandler, consider using an alternative interface called TDTM_RunnableMutable.
SetFieldsOnAccount Apex Class
The code signature for implementing TDTM_RunnableMutable is very similar to TDTM_Runnable. Let's look at a code example.
In this example, we want the NumberofEmployees field on the Account object to increase by one every time a Contact is created for a given Account.
global class CON_SetFieldsOnAccount_TDTM_Mutable extends npsp.TDTM_RunnableMutable {
global override void run(List<SObject> listNew, List<SObject> listOld,
npsp.TDTM_Runnable.Action triggerAction, Schema.DescribeSObjectResult objResult,
npsp.TDTM_Runnable.DmlWrapper dmlWrapper) {
// cast new contacts
List<Contact> newRecords = (List<Contact>) listNew;
if(triggerAction == npsp.TDTM_Runnable.Action.AfterInsert) {
// create collection to hold Account IDs
Set<Id> accountIds = new Set<Id>();
// iterate over new records to collect Account IDs
for(Contact newRecord : newRecords) {
accountIds.add(newRecord.AccountId);
}
// retrieve those accounts
Map<Id, Account> accounts = getAccounts(accountIds);
// create a map version of dmlWrapper's objectsToUpdate
Map<Id, SObject> objectsToUpdateMap = new Map<Id, SObject>(dmlWrapper.objectsToUpdate);
// iterate over contacts to set each Account's NumberOfEmployees
for(Contact newRecord : newRecords) {
if(accounts.containsKey(newRecord.AccountId)) {
/*
check whether this account is already in dmlWrapper
as a record that will receive an update
*/
Account account;
if(objectsToUpdateMap.containsKey(newRecord.AccountId)) {
account = (Account) objectsToUpdateMap.get(newRecord.AccountId);
} else {
account = accounts.get(newRecord.AccountId);
}
// if this is null, initialize to 0
if(account.NumberOfEmployees == null) {
account.NumberOfEmployees = 0;
}
// increment by 1, as this contact is part of the account
account.NumberOfEmployees += 1;
// put this account into the collection of records to update
objectsToUpdateMap.put(account.Id, account);
}
}
// update dmlWrapper's objectsToUpdate to reflect this collection
dmlWrapper.objectsToUpdate = objectsToUpdateMap.values();
}
}
public static Map<Id, Account> getAccounts(Set<Id> recordIds) {
return new Map<Id, Account>([SELECT Id, NumberOfEmployees FROM Account WHERE Id IN :recordIds]);
}
}RunnableMutable Trigger Handler
In this code example, we see that the CON_SetFieldsOnAccount_TDTM_Mutable class extends TDTM_RunnableMutable, not TDTM_Runnable. The class checks to see if the Account record from the trigger is already in dmlWrapper.objectsToUpdate and updates the record, or places the Account retrieved into that collection.
Create a Trigger Handler record for each class that implements TDTM_RunnableMutable. Using the CON_SetFieldsOnAccount_TDTM_Mutable class example, the trigger handler would look like this:
npsp__Trigger_Handler__c triggerHandler = new npsp__Trigger_Handler__c();
triggerHandler.Name = 'CON_SetFieldsOnAccount_TDTM_Mutable';
triggerHandler.npsp__Active__c = true;
triggerHandler.npsp__Asynchronous__c = false;
triggerHandler.npsp__Class__c = 'CON_SetFieldsOnAccount_TDTM_Mutable';
triggerHandler.npsp__Load_Order__c = 2;
triggerHandler.npsp__Object__c = 'Contact';
triggerHandler.npsp__Trigger_Action__c = 'AfterInsert';
insert triggerHandler;