Did you know that you can dynamically disable fflib_SObjectDomain
Trigger Handlers? You did? Ok then, did you know that when you do disable them their constructors run anyway? So be careful not to put any (expensive) logic inside those.
For instance, I had some common related records that needed to be loaded from the Database in multiple contexts. Something like getting the related Accounts in the Contact insert and update operations to do some custom validation. I collected Ids and fetched the records in the constructor so as not to duplicate the code.
My real scenario was of course a lot fancier (and expensive), but to illustrate:
public with sharing class ContactsTriggerHandler extends fflib_SObjectDomain {
private Map<Id, Account> accountMap;
public ContactsTriggerHandler(List<Contact> contacts) {
super(contacts, Contact.sObjectType);
Set<Id> accountIds = new Set<Id>();
for (Contact contact : contacts) {
accountIds.add(contact.AccountId);
}
accountMap = TriggerContextCache.getAccounts(accountIds);
}
public override void onValidate() {
checkAccountNotBlocked();
}
public override void onValidate(Map<Id, SObject> existingRecords) {
checkAccountNotBlocked();
}
private void checkAccountNotBlocked() {
for (Contact contact : (List<Contact>) getRecords()) {
if ('Blocked' == accountMap.get(contact.AccountId)?.Description) {
contact.addError('Account is blocked');
}
}
}
public class Constructor implements fflib_SObjectDomain.IConstructable {
public fflib_SObjectDomain construct(List<SObject> sObjectList) {
return new ContactsTriggerHandler(sObjectList);
}
}
}
I had some frequent logic that didn’t need to be subject to validation or any other processing that was handled by the triggers. So to save on execution time (especially with large imports) it did its DMLs after disabling the triggers.
Id accountId = [SELECT Id FROM Account LIMIT 1].Id;
fflib_SObjectDomain.getTriggerEvent(ContactsTriggerHandler.class).disableAll();
insert new Contact(LastName = 'Test', AccountId = accountId);
It didn’t seem to save as much time as I thought it would, because some of the big SOQL queries (and more) were running anyway. Look at the logs from my simplified example: the Account SOQL query was executed even though I don’t need it. This could mean a lot of wasted (CPU) time in more complex situations.
Moving the cache loading out of the constructor helps with that. It may just need a little more love to make it elegant sometimes.
And be careful where it is placed as each Trigger Event calls different methods (e.g. onApplyDefaults()
only on insert but before onValidate()
). You could run into some NullPointer
Exceptions.
public with sharing class ContactsTriggerHandler extends fflib_SObjectDomain {
private Map<Id, Account> accountMap {
get {
if (accountMap == null) {
Set<Id> accountIds = new Set<Id>();
for (Contact contact : (List<Contact>) getRecords()) {
accountIds.add(contact.AccountId);
}
accountMap = TriggerContextCache.getAccounts(accountIds);
}
return accountMap;
}
set;
}
public ContactsTriggerHandler(List<Contact> contacts) {
super(contacts, Contact.sObjectType);
}
...
}
Running the same script again and now the SOQL query is no longer executed. Happy days!
Other Traps
LWC in Purgatory
If you are delivering your SFDX projects using packaging you are likely aware of the challenges with moving metadata artefacts from one package to another. Particularly when moving up the dependency hierarchy. There is this very neat “trick” for Unlocked Packages where you can temporarily remove metadata from a package…