Managing Triggers in Salesforce with Gonkulator
Gonkulator
The purpose of the Gonkulator is to provide a framework for managing Apex Trigger code on Salesforce.com (SFDC).
Taking Back Control of your salesforce org
Login to your Org, navigate to SetUp -> Apex Code. Scan through the list of classes, how many different classes do you see that are suffixed with “Handler” or “Helper?” These classes are the result of your well-intended and over-allocated developers/consultants to implement Salesforce.com using “best practices” by following tribal coding patterns. If your Org is greater than five years old, and you’ve had greater than two developers/consultants “customizing” your Org, what you’ve got is a spaghetti code mess. If you're one of the lucky ones that required documentation, there’s a 23 percent chance the documentation actually reflects what is implemented in the code. Even a seasoned developer reading through your biblical code library is going to be relegated to “educated guesses” to give it all meaning.
Every “Certified Developer” you hire will regurgitate the same party line of “bulkification,” “abstraction,” and “iterative development,” at the end of the day they will only contribute to the abyss of your code repository.
The greatest advantage of Salesforce.com is its ease of customization. Its greatest determent is the same. I’ve spent the better part of 20 years, implementing, designing, solutioning, architecting, re-implementing, re-designing, re-architecting, re-solutioning Salesforce.com and every engagement starts the same, “helps us understand what’s happening in our Org.”
What’s needed is a framework that will allow us to develop business logic independent of the code base. It needs to be self-documenting, we should be able to monitor the code to know if it’s being executed, and be able to easily retire outdated code, we should be able to preemptively know when a piece of code is reaching its governor limits, we should be able to turn on and off code at a moment’s notice, and we should not be beholden to the developers and consultants for the health of our Org! But how?
Enter the Gonkulator!
The Gonkulator is a Salesforce.com trigger coding framework that allows you to control your code, instead of the code controlling you. It gives you visibility into what apex code is critical to your business operations and what code can be removed. It provides a real time dashboard of invocations, governor limits and errors. It allows you to turn on and turn off code without the need for a deployment. It gives you the ability to incorporate offshore development resources and even AI into your code development without exposing core trigger logic.
How it works - technical details
At the heart is the Gonkulator__c Object and an Apex Interface called the Gonkiface.
The Gonkulator__c object acts as your Trigger Operations Center, it give you the ability to perform the following actions without the need for Code deployment:
- When a trigger is fired, i.e. after insert, after update, before insert, before update, before delete, after delete.
- To turn on and turn off a trigger.
- Which code gets executive.
- The order in which code is executed.
- Options for how to handle errors.some text
- Ignore the error and move on.
- Throw and error to the user.
- Governor limit used.
- Time of execution
- Number of executions
- Timestamp of last execution
- Documentation about the technical and business uses for the code.
- Ability to turn on and off Debugging messages to pinpoint production errors.
Gonkiface
The Apex that you write for the Gonkulator is standard SFDC Apex, it just implements the Gonkiface Interface. This interface requires you implement five methods in your Apex class and put your business logic in the proper method. The five methods are init, getData, validate, execute, dml.
- Init() – this passing into your class meta data, and data about the Trigger. So if you’re writing a Gonkultor interface for an Account it will provide for you all affected Accounts, and, if necessary, parent and child objects associated with the affected Accounts.
- getData() – if you need data that is not directly related to an Account via a Parent or child relationship, you can put your SOQL queries in this method. As it is only called once this will insure you are not hitting SOQL governor limits
- validate() – use this to deep validation, where validation rules fall short, and you need to validate deep into parent and child relationships.
- Execute() – this is the “main” method where you put your business logic
- Dml() – called once so that you can update and insert new records without the worry about recursion.
Sample of the interface is below:
public interface Gonkiface {
/*
* The GonkulatorModel.Gonk is an inner class that holds all the information to passed INTO the Gonkulator
* i.e. The list of Task that are being updated. It also Holds the result of what happened during execution
* of the Gonkulator so you pass error messages back to the User
*
* Your implementation of the Gonkiface should declare:
* GonkulatorModel.Gonk vGonk = new GonkulatorModel.Gonk();
* You init method should simple set the Class variable
*
* void init(GonkulatorModel.Gonk incomingGonk){
* vGonk=incomingGonk;
* }
*/
void init(GonkulatorModel.Gonk vGonk);
/*
* Use the getData() Method to do any SOQL Queries and store the result in Class
* Vaiable for Use in Other methods
*/
void getData();
/*
* The Validate() methods is used for you to check integreity of the data and throw erros back to the user
* if need
*/
void validate();
/*
* This execute() method is where you perform you business logic and collect Objects to be updated
* or inserted.
*
* GonkulatorModel.Gonk two fields for you to accumulated DML data:
* objectsToUpdate
* objectsToInsert
*
*/
void execute();
/*
* dml() method is where you call all your update and insert DMLs
*/
void dml();
}
invoking the Gonkulator
To add the Gonkulator to any Object’s you’ll need to create a Trigger that invokes the framework
trigger InventoryTrigger on wickby__Inventory__c (after delete, after insert, after update, before delete, before insert, before update) {
GonkFactory.gonkulate(Trigger.New);
}
That’s it. You can now beginning writing and deploying Gonkulator classes.
Gonkulator increases your Team's development velocity
If you’re like most organization, you may be utilizing offshore development resource. One of the challenges is that business logic often requires that develops touch the “controller” code of your triggers. Not any more. With the Gonkulator, an Apex class contains just the necessary logic to solve a specific business requirement, no decencies on other code. As such you can send the requirements to your offshore team, and they can return to you a single Apex Class, and corresponding Test Class with 100 percent code coverage, that you simple “plug in” to the Gonkulator__c. You de-risk deployments because you never deploy the “controlling” trigger code; you only ever have to deploy single, self contained apex classes that can be turned on and off with the click of a box.
User stories can not map to a single apex class, and when the user story becomes obsolete, you’ll know exactly which code within your org you can safely retire.
Say Goodbye to 'too many soql queries'
If you’re following SFDC “best practices,” then you have “bulkified” your code. Which is geek for making sure you don’t hit the dreaded governor limit of ‘too many soql queries’ which can be the death nail to any Org. When a SFDC trigger fires it will give your code a list of all of the objects that it needs, for example if you’re updating an Order Item it will give a list of all the fields and the values that are being inserted/updated in the database for the single Order Item (or 200 Order Items if you’re doing a dataload). But it doesn’t not give you the values for all the other related Object, i.e. the Order that contains the Order Items, nor the Opportunity that contains the Order nor the Account that Contains the Opportunity That is the responsibility of the programmer. As a result, each of your trigger that require deeper queries, or require new fields you are potentially adding another query or changing the existing queries to accommodate new fields.
With the Gonkulator you have the ability to configure it to retrieve all fields from all child objects and Five levels of Parents, with ONE soql query. That means with a single line of code you could get all fields and all values for an Order, all its related order Items, the Order’s Opportunity, the Opportunity’s Account, the Account’s Owner and Account Owner’s Manager’s email address Using a single Soql Query. What’s more you don’t have to worry about writing all the SOQL Statement the gonkulator takes care of all of that for you.
Example:
vAccountQuery = new GonkulatorModel.Query();
vGonk.queryMap.put(Schema.SObjectType.Account.getSObjectType(), vAccountQuery);
vAccountQuery.children.add('Contacts');
vAccountQuery.children.add('Opportunities');
vAccountQuery.parent.add('parentAccount');
vAccountQuery.parent.add('parentAccount.parentAccount');
vAccountQuery.criteriaField='Id';
vGonk.queryMap.put(Schema.SObjectType.Account.getSObjectType(),vAccountQuery);
GonkulatorCore.gonkQuery(vGonk);
The above code example shows you how you would query an Account Object to get all the related Contacts and Opportunities, and the Account’s Parent Account, and the Account’s Grandparent Account.
Test Class and Test data
The modular nature of the Gonkulator has the added benefit of streamline test class creation and test data creation.
When creating Apex test classes the biggest challenge is to create enough test data so that the test class executes. This can be a daunting task especially if your business logic is going across multiple object. Creating test data with Gonkulator is fast and thorough, because the Gonkulator comes with is own Object generator. It will populate every field (standard and custom) of an object with sample data of the correct type, i.e. string, number, email, address, etc. and return to you a fully populated object. Because it is dynamically populating the object you never have to worry about if fields are added or removed the test class will work (unless it needs to fail ). Test classes become true “TEST” of your code, not just code coverage.
The below code example shows how you can create fully populated Account, Contact and Opportunity Relationships.
Account vAccount = (Account)GonkulatorCore.populateObjectData(Account.getSObjectType());
Insert vAccount;
Contact vContact =(Contact)GonkulatorCore.populateObjectData(Schema.SObjectType.Contact.getSObjectType());
vContact.AccountId=vAccount.Id;
insert vContact;
Opportunity vOpportunity = (Opportunity)GonkulatorCore.populateObjectData(Schema.SObjectType.Opportunity.getSObjectType());
vOpportunity.AccountId=vAccount.Id;
Insert vOpportunity;
What’s more the same method can be used to create seed data within sandboxes – but more on that later.
Order of Execution
Being able to control the Order of execution has long been a known problem on SFDC that we have all learned to program around. With the Gonkulator, you can now control the order in which code is fired and can even have conditional logic where one piece of code is dependent upon the success or failure of a second class.
If One Fails the rest can succeed
Even if you are using “best practices” as prescribed by most SFDC developers, because control of your trigger is being executed by a single “controller” class, if one of the lines, or “helper” classes fail, then the entire trigger fails, and the user will be stuck.
There are use cases where some functionality with in the trigger is more business critical than other. For example, say part of your Contact trigger was to copy the phone number field to the mobile phone field, and within the same trigger you had logic that flags the Contact as a High valued customer. If the phone number doesn’t get populated, no harm no foul, but missing the high value flag could mean lost revenue. With traditional apex patterns, if an integration like Marketo or Marketing cloud where to send a bad phone number, then the Trigger would fail and the integration would stop, never getting to the important functionality of flagging the contact.
With the Gonkulator you decide the importance of each piece of business logic. From your Triggers Operation Control Panel you can designate the behavior of an error condition, you can choose to pass that error up to the User Interface, or have the Gonkulator log the error and carrying on with the additional business logic
Controlling recursion
There are many scenarios that require an Object’s Trigger to update the very Object that cause it to fire. If not handled properly then could end up with a “recursion” condition. For example, upon insert of an Account, if the Account is located on the West Coast, Update the Account Owner to be the Vice President of Sales for the Western Region. The Trigger needs to update the Account, which could then cause the same trigger to fire again, which would update the Account which would cause it to fire again, etc., etc.
The Gonkulator automatically disables itself the second time through, so that if you need to update the same object you’re currently processing it will not fire a second time, preventing hitting a number of SFDC Governor limits.
Using ChatGPT to Create Apex Classes
With a framework by which to implement Apex triggers, and an Apex Interface to standardize how code is written, we now have everything necessary to train AI to write our Apex Triggers for us. You simply Tell AI the logic you want, take the output and plug it into the Gronkulator Object.
Sample Implementation - Creating a Rollup Summary Field
Let’s take a common use case. Roll Up Summary on objects that are grand parents or great grand parents of an object. Let’s create a Gonkulator class that can do a sum of all orderItems and stamp the value on a field on the Account.
public without sharing class RollUpSummary_GonkImpl implements Gonkiface {
public static GonkulatorModel.Gonk vGonk;
public static GonkulatorModel.Query vOrderQuery;
/*
* The GonkulatorModel.Gonk is an inner class that holds all the information to passed INTO the Gonkulator
* i.e. The list of Task that are being updated. It also Holds the result of what happened during execution
* of the Gonkulator so you pass error messages back to the User
*
* Your implementation of the Gonkiface should declare:
* GonkulatorModel.Gonk vGonk = new GonkulatorModel.Gonk();
* You init method should simple set the Class variable
*
* void init(GonkulatorModel.Gonk incomingGonk){
* vGonk=incomingGonk;
* }
*/
public void init(GonkulatorModel.Gonk incomingGonk){
vGonk=incomingGonk;
}
/*
* Use the getData() Method to do any SOQL Queries and store the result in Class
* Vaiable for Use in Other methods
*/
public void getData(){
if(vOrderQuery==null){
vOrderQuery = new GonkulatorModel.Query();
vGonk.queryMap.put(Schema.SObjectType.Order.getSObjectType(), vOrderQuery);
vOrderQuery.children.add('OrderItems');
vOrderQuery.parents.add('Order.Opportunity');
vOrderQuery.parents.add('Order.Opportunity.Account');
vOrderQuery.parents.add('Order.Account');
vOrderQuery.criteriaField='Id';
GonkulatorCore.gonkQuery(vGonk);
}
}
/*
* The Validate() methods is used for you to check integreity of the data and throw erros back to the user
* if need
*/
public void validate(){
}
/*
* This execute() method is where you perform you business logic and collect Objects to be updated
* or inserted.
*
* GonkulatorModel.Gonk two fields for you to accumulated DML data:
* objectsToUpdate
* objectsToInsert
*
*/
public void execute(){
List<Account> accountsToUpdate = new List<Account>();
if(vOrderQuery!=null){
for(sobject sObj : vGonk.queryMap.get(Schema.SObjectType.Order.getSObjectType()).queryResults){
Order vOrder = (Order)sObj;
Decimal totalOrderAmount = 0;
for(OrderItem vOrderItem : vOrder.OrderItems){
totalOrderAmount=totalOrderAmount+(vOrderItem.UnitPrice*vOrderItem.Quantity);
}
system.debug(vOrder.Opportunity);
system.debug(vOrder.Opportunity.Account.Name);
Account vAccount= new Account();
vAccount.Id=vOrder.Opportunity.Account.Id;
vAccount.gonkulator__Total_Orders__c=totalOrderAmount;
vGonk.objectsToUpdate.add(vAccount);
}
}
}
/*
* dml() method is where you call all your update and insert DMLs
*/
public void dml(){
if(!vGonk.objectsToUpdate.isEmpty()){
update vGonk.objectsToUpdate;
}
}
}
Let Walk through the code:
getData()
This method is going to query all of the fields on the OrderItem, the Order, the Opportunity, the Account associated to the Opportunity, and the Account Associated to the Order. And it will get all of those fields for you with a single query.
The Gonkulator has a build in Query engine, you just tell it what you want and it write all of the SQL for you.
public void getData(){
if(vOrderQuery==null){
vOrderQuery = new GonkulatorModel.Query();
vGonk.queryMap.put(Schema.SObjectType.Order.getSObjectType(), vOrderQuery);
vOrderQuery.children.add('OrderItems');
vOrderQuery.parents.add('Order.Opportunity');
vOrderQuery.parents.add('Order.Opportunity.Account');
vOrderQuery.parents.add('Order.Account');
vOrderQuery.criteriaField='Id';
GonkulatorCore.gonkQuery(vGonk);
}
}
In this example the SOQL that gets executed would look like the following. But you don’t have to write a single line.
SELECT Id, OwnerId, ContractId, Account.Name, Account.Type, Account.ParentId, Account.BillingStreet, Account.BillingCity, Account.BillingState, Account.BillingPostalCode, Account.BillingCountry, Account.BillingLatitude, Account.BillingLongitude, Account.BillingGeocodeAccuracy, Account.ShippingStreet, Account.ShippingCity, Account.ShippingState, Account.ShippingPostalCode, Account.ShippingCountry, Account.ShippingLatitude, Account.ShippingLongitude, Account.ShippingGeocodeAccuracy, Account.Phone, Account.Fax, Account.AccountNumber, Account.Website, Account.Sic, Account.Industry, Account.AnnualRevenue, Account.NumberOfEmployees, Account.Ownership, Account.TickerSymbol, Account.Description, Account.Rating, Account.Site, Account.OwnerId, Account.Jigsaw, Account.CleanStatus, Account.AccountSource, Account.DunsNumber, Account.Tradestyle, Account.NaicsCode, Account.NaicsDesc, Account.YearStarted, Account.SicDesc, Account.DandbCompanyId, Account.OperatingHoursId, Account.gonkulator__Total_Orders__c, Pricebook2Id, OriginalOrderId, Opportunity.AccountId, Opportunity.IsPrivate, Opportunity.Name, Opportunity.Description, Opportunity.StageName, Opportunity.Amount, Opportunity.Probability, Opportunity.TotalOpportunityQuantity, Opportunity.CloseDate, Opportunity.Type, Opportunity.NextStep, Opportunity.LeadSource, Opportunity.ForecastCategoryName, Opportunity.CampaignId, Opportunity.Pricebook2Id, Opportunity.OwnerId, Opportunity.Account.Name, Opportunity.Account.Type, Opportunity.Account.ParentId, Opportunity.Account.BillingStreet, Opportunity.Account.BillingCity, Opportunity.Account.BillingState, Opportunity.Account.BillingPostalCode, Opportunity.Account.BillingCountry, Opportunity.Account.BillingLatitude, Opportunity.Account.BillingLongitude, Opportunity.Account.BillingGeocodeAccuracy, Opportunity.Account.ShippingStreet, Opportunity.Account.ShippingCity, Opportunity.Account.ShippingState, Opportunity.Account.ShippingPostalCode, Opportunity.Account.ShippingCountry, Opportunity.Account.ShippingLatitude, Opportunity.Account.ShippingLongitude, Opportunity.Account.ShippingGeocodeAccuracy, Opportunity.Account.Phone, Opportunity.Account.Fax, Opportunity.Account.AccountNumber, Opportunity.Account.Website, Opportunity.Account.Sic, Opportunity.Account.Industry, Opportunity.Account.AnnualRevenue, Opportunity.Account.NumberOfEmployees, Opportunity.Account.Ownership, Opportunity.Account.TickerSymbol, Opportunity.Account.Description, Opportunity.Account.Rating, Opportunity.Account.Site, Opportunity.Account.OwnerId, Opportunity.Account.Jigsaw, Opportunity.Account.CleanStatus, Opportunity.Account.AccountSource, Opportunity.Account.DunsNumber, Opportunity.Account.Tradestyle, Opportunity.Account.NaicsCode, Opportunity.Account.NaicsDesc, Opportunity.Account.YearStarted, Opportunity.Account.SicDesc, Opportunity.Account.DandbCompanyId, Opportunity.Account.OperatingHoursId, Opportunity.Account.gonkulator__Total_Orders__c, EffectiveDate, EndDate, IsReductionOrder, Status, Description, CustomerAuthorizedById, CustomerAuthorizedDate, CompanyAuthorizedById, CompanyAuthorizedDate, Type, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry, BillingLatitude, BillingLongitude, BillingGeocodeAccuracy, BillingAddress, ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry, ShippingLatitude, ShippingLongitude, ShippingGeocodeAccuracy, ShippingAddress, Name, PoDate, PoNumber, OrderReferenceNumber, BillToContactId, ShipToContactId, ActivatedDate, ActivatedById, StatusCode, OrderNumber, TotalAmount, CreatedDate, CreatedById, LastModifiedDate, LastModifiedById, IsDeleted, SystemModstamp, LastViewedDate, LastReferencedDate, (SELECT Quantity, UnitPrice, ServiceDate, EndDate, Description FROM OrderItems) FROM Order WHERE Id IN ('801Ov00000Cqq72IAB')
execute()
This is where all the logic since the Gonkulator.queryMap has all of the data retrieved from the getData method we are free to loop through the order items, and update the custom field on the Account
public void execute(){
List<Account> accountsToUpdate = new List<Account>();
if(vOrderQuery!=null){
for(sobject sObj : vGonk.queryMap.get(Schema.SObjectType.Order.getSObjectType()).queryResults){
Order vOrder = (Order)sObj;
Decimal totalOrderAmount = 0;
for(OrderItem vOrderItem : vOrder.OrderItems){
totalOrderAmount=totalOrderAmount+(vOrderItem.UnitPrice*vOrderItem.Quantity);
}
system.debug(vOrder.Opportunity);
system.debug(vOrder.Opportunity.Account.Name);
Account vAccount= new Account();
vAccount.Id=vOrder.Opportunity.Account.Id;
vAccount.gonkulator__Total_Orders__c=totalOrderAmount;
vGonk.objectsToUpdate.add(vAccount);
}
}
}
GonkulatorModel.Gonk Inner Class
The modular nature of the Gonkulator has the added benefit of streamline test class creation and test data creation.
This Inner class is used to pass data into the Gonkulator framework and between Gonkulators. The init() method of your class will receive an instantiation of this class already populated. Let look at each of the values:
For Each Gonkulator__c Object there is an associated GonkulatorModel.Gonk inner class at runtime.
public class Gonk{
public String status='';
public Map<sObjectType, Query> queryMap = new Map<sObjectType, Query>();
public List<SObject> objectList = new List<sObject>();
public Map<String, sObject> objectMap;
public String className;
public Gonkulator__c gonkulatorRecord= new Gonkulator__c();
public List<Gonkulator_Log__c> gonkulatorLogList = new List<Gonkulator_Log__c>();
public Map<String, String> executionStats = new Map<String,String>();
public Map<String, Object> parameters = new Map<String, Object>();
public List<String> errorMessages= new List<String>();
public List<SObject> objectsToUpdate = new List<SObject>();
public List<SObject> objectsToInsert = new List<sObject>();
public void parseJSON(String vJson){
try{
parameters = (Map<String, Object>)JSON.deserializeUntyped(vJson);
} catch (Exception e) {
System.debug('parseJson error: '+e.getMessage());
}
}
public Map<String, sObject> getObjectMap(String sortField){
Map<String, sObject> newMap = new Map<String, sObject>();
if (objectList != null) {
for(sObject vObj : objectList){
if(vObj.get(sortField)!=null){
newMap.put((String)vObj.get(sortField), vObj);
}
}
}
return newMap;
}
public Map<String, sObject> getObjectMap(){
if (objectMap==null && objectList != null) {
objectMap= new Map<String, sObject>();
for(sObject vObj : objectList){
if(vObj.Id!=null){
objectMap.put(vObj.Id, vObj);
}
}
}
return objectMap;
}
}
gonkulatorRecord
A copy of the Gonkulator__c object
Parameters
The Gonkulator__c Object carries a text area that you can include parameters in to pass into your Gonkulator Interface. There are some reserved parameters for convenience
TURN_GONK_TRIGGER_OFF_FOR_OBJECTTYPE
If the TURN_GONK_TRIGGER_OFF_FOR_OBJECTTYPE parameter is passed into Gonkulator__c object, Then no Gonkulators will fire for the given Object Type name during the Gonkulators dml() method.
{
"objectName":"Order",
"dependantGonk":"RollUpSummary_GonkImpl",
"TURN_GONK_TRIGGER_OFF_FOR_OBJECTTYPE":"Account"
}
In the Above example, If there’s a Gonkulator__c defined for APIName equals “Account,” and the current Gonkulator has a condition that updates Account Object, then the defined Gokulators will no be executed.
Similarly, you can use the TURN_GONK_TRIGGER_OFF parameter to specific specific Gonkulator Class that you do not want to fire, as a result the a DML action in the current Gonkulator
{
"objectName":"Order",
"TURN_GONK_TRIGGER_OFF":"Demo_GonkImpl"
}
Interdependent Gonkulators
You can make one Gokulatores executing be dependent upon the success or failure of another Gonkulator.