Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Salesforce trigger notes

21 Jan 2022 🔖 salesforce
💬 EN

Table of Contents

This morning I read Fernando Doglio’s I’ll admit it, I’ve never had to reverse a linked list in my career and realized I wanted to write my own love letter to knowledge piled on top of knowledge.

Fernando wrote:

That was it, I had peaked.

It took me a while to realize I hadn’t. Through conversations with my teammates, I started seeing how they used concepts I had deemed unnecessary throughout my learning path. Things like design patterns — I didn’t need any design patterns; I knew everything I needed.

And yes, that is partially true. I didn’t need them, I could work without them, but was I doing the best job I could without them? The answer is no, I wasn’t.

When I went to grad school, my “programming 102” course covered a bunch of object-oriented design patterns, and our final project was to work with someone else to design a working piece of software that implemented at least … 4, I think? … of them, with encouragement to slip in more if we could.

When I hit my professional career making business-logic “trigger” code that ran inside of database management systems like Oracle and Salesforce, that felt like abstract knowledge.

Sadly, when I started programming in Apex, I misunderstood “one trigger per object” as “one trigger handler per object.”

No.

The amount of CPU time it takes to re-instantiate objects and variables is nothing compared to the amount of human time it takes to edit a messy codebase.

I’m so grateful for the time I spent learning design patterns in school, because they made it possible for me to engage with experienced Salesforce developers and follow along when reading books like Andy Fawcett’s or like Joshua Bloch’s Effective Java when other developers referred to chapters from it to give depth to their answers to the questions I asked.

So … here’s what I’m up to so far, from the spaghetti code I started writing just to get things done as a baby dev:


I use TDTM now

I’ve been working in the NPSP / EDA context lately, so while there are a million trigger frameworks in existence, one of the biggest shifts for me, which I picked up from a colleague, was switching to having triggers that simply look like this:

trigger CustomObjectTrigger on Custom_Object__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.Custom_Object__c);
}

And then a bunch of trigger handlers, each of which look like this:

global class zerothproject_TDTM_CustomObject extends hed.TDTM_Runnable {
    // ------- Methods, Public/Global -------

    // 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 returnMe = NULL;
        
        if ( triggerAction == hed.TDTM_Runnable.Action.BeforeInsert ) {
            if ( newlist == NULL || newlist.isEmpty() ) { return returnMe; }
            List<Custom_Object__c> newCOs = (List<Custom_Object__c>)newlist;
            for ( Custom_Object__c co : newCOs ) {
                editArea(co); // Edit 1 of 2.
                clearForceCheckbox(co); // Edit 2 of 2.  Make sure this happens last
            }
        } // End "BEFORE INSERT"
        
        if ( triggerAction == hed.TDTM_Runnable.Action.BeforeUpdate ) {
            if ( newlist == NULL || newlist.isEmpty() || oldlist == NULL ) { return returnMe; }
            List<Custom_Object__c> newCOs = (List<Custom_Object__c>)newlist;
            Map<Id, Custom_Object__c> triggerOldMap = new Map<Id, Custom_Object__c>((List<Custom_Object__c>)oldlist);

            // Start caching things that changed
            List<Custom_Object__c> needsAlert = new List<Custom_Object__c>();
            List<Custom_Object__c> needsAreaRecompute = new List<Custom_Object__c>();
            for ( Custom_Object__c co : newCOs ) {
                Custom_Object__c oldCO = triggerOldMap.get(co.Id);
                if ( 
                    co.City__c != oldCO.City__c ||
                    co.State__c != oldCO.State__c ||
                    co.Zip__c != oldCO.Zip__c ||
                    co.Country__c != oldCO.Country__c ||
                    co.Custom_Field_1__c != oldCO.Custom_Field_1__c
                ) {
                    needsAlert.add(co); // Queue up the custom object for editing the "something changed" flag that the data quality team monitors the value of
                }
                if ( 
                    co.Force_GeoArea_Recompute__c || // Subtle difference from above
                    co.City__c != oldCO.City__c ||
                    co.State__c != oldCO.State__c ||
                    co.Zip__c != oldCO.Zip__c ||
                    co.Country__c != oldCO.Country__c
                ) {
                    needsAreaRecompute.add(co); // Queue up the custom object for editing geographic area
                }
            } // End caching things that changed

            for ( Custom_Object__c coThatNeedsAlert : needsAlert ) {
                editFlag(coThatNeedsAlert); // Edit 1 of 3.
            }
            for ( Custom_Object__c coThatNeedsRecompute : needsAreaRecompute ) {
                editArea(coThatNeedsRecompute); // Edit 2 of 3.
            }
            for ( Custom_Object__c co : newCOs ) {
                clearForceCheckbox(co); // Edit 3 of 3.  Make sure this happens last.
            }
        } // End "BEFORE UPDATE"
        
        return returnMe;
    }
    
    // ------- Helper Methods -------
    
    private static Boolean editFlag(Custom_Object__c co) {
        // This method modifies the SObject you pass it whether or not you make use of the Boolean it returns.
        Boolean changedAnything = FALSE;
        if ( !co.ProcessZero_Involved_Field_Changed__c ) { 
            co.ProcessZero_Involved_Field_Changed__c = TRUE;
            changedAnything = TRUE;
        }
        return changedAnything;
    }

    private static Boolean editArea(Custom_Object__c co) {
        // This method modifies the SObject you pass it whether or not you make use of the Boolean it returns.
        Boolean changedAnything = FALSE;
        // Convert custom object address into a geographic area suggestion
        Geographic_Area__c suggestedGeographicArea = firstproject_GeoAreaSelector.suggestGeoAreaFromAddress(co.City__c, co.State__c, co.Zip__c, co.Country__c);
        Id suggestedGeographicAreaId = suggestedGeographicArea?.Id;
        // Write the suggestion to the custom object SObject
        if ( co.Address_Geographic_Area__c != suggestedGeographicAreaId ) {
            co.Address_Geographic_Area__c = suggestedGeographicAreaId;
            changedAnything = TRUE;
        }
        return changedAnything;
    }

    private static Boolean clearForceCheckbox(Custom_Object__c co) {
        // This method modifies the SObject you pass it whether or not you make use of the Boolean it returns.
        Boolean changedAnything = FALSE;
        if ( co.Force_GeoArea_Recompute__c ) { 
            co.Force_GeoArea_Recompute__c = FALSE;
            changedAnything = TRUE;
        }
        return changedAnything;
    }

}

or like this:

global without sharing class firstproject_TDTM_CustomObject extends hed.TDTM_Runnable {
    
    // ------- Attributes, Static -------
    private static Set<Id> idsAlreadySetAsideForProcessing = new Set<Id>(); // "Ever queued, any pass"
    private static @TestVisible Boolean gotToInsertCheckpoint = FALSE; // TestVisible for simple "did we enter the trigger handler?" unit test (biz logic tested elsewhere)
    private static @TestVisible Boolean gotToUpdateCheckpoint = FALSE; // TestVisible for simple "did we enter the trigger handler?" unit test (biz logic tested elsewhere)
    private static FINAL String RECORDLOCKMESSAGE = 'UNABLE_TO_LOCK_ROW';
    private static @TestVisible Boolean throwFakeRecordLockException = FALSE;
    
    // ------- Attributes, Instance -------
    private Set<Id> processTheseIds = new Set<Id>(); // "This-pass" queue
    
    // ------- Methods, Static, Public -------
    public static Boolean throwingFakeRecordLockException() { return throwFakeRecordLockException; } // firstproject_CustomObject needs to be able to see this

    // ------- Methods, Instance, Global -------

    // 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 returnMe = NULL;
        if ( System.isFuture() ) { return returnMe; } // If this trigger-handler is in a future context, skip it (its only job is to invoke more future, which isn't possible)
        
        if ( triggerAction == hed.TDTM_Runnable.Action.AfterInsert ) {
            if ( newlist == NULL ) { return returnMe; }
            List<Custom_Object__c> newCOs = (List<Custom_Object__c>)newlist;
            for ( Custom_Object__c co : newCOs ) {
                Id coID = co.Id;
                processTheseIds.add(coID); // Queue up the custom object for future-edit
                idsAlreadySetAsideForProcessing.add(coID); // Static-cache the custom object as queued already
            }
            if ( !processTheseIds.isEmpty() ) {
                if ( System.isBatch() ) {
                    propagateDataIfRelevant(processTheseIds); // Call the now-method against the queue
                }
                else {
                    propagateDataIfRelevantFuture(processTheseIds); // Call the future-method against the queue
                }
                gotToInsertCheckpoint = TRUE;
            }
        } // End "AFTER INSERT"
        
        if ( triggerAction == hed.TDTM_Runnable.Action.AfterUpdate ) {
            if ( newlist == NULL || oldlist == NULL ) { return returnMe; }
            List<Custom_Object__c> newCOs = (List<Custom_Object__c>)newlist;
            Map<Id, Custom_Object__c> triggerOldMap = new Map<Id, Custom_Object__c>((List<Custom_Object__c>)oldlist);
            for ( Custom_Object__c co : newCOs ) {
                Id coID = co.Id;
                Custom_Object__c oldCO = triggerOldMap.get(coID);
                if ( idsAlreadySetAsideForProcessing.contains(coID) ) { continue; } // No need to re-queue this custom object
                if ( 
                    co.Custom_Field_1__c == oldCO.Custom_Field_1__c &&
                    co.Custom_Field_2__c == oldCO.Custom_Field_2__c &&
                    co.Custom_Field_3__c == oldCO.Custom_Field_3__c
                ) {
                    continue; // Ignore this custom object if no relevant fields changed
                }
                processTheseIds.add(coID); // Queue up the custom object for future-edit
                idsAlreadySetAsideForProcessing.add(coID); // Static-cache the custom object as queued already
            } // End caching things that changed
            if ( !processTheseIds.isEmpty() ) {
                if ( System.isBatch() ) {
                    propagateDataIfRelevant(processTheseIds); // Call the now-method against the queue
                }
                else {
                    propagateDataIfRelevantFuture(processTheseIds); // Call the future-method against the queue
                }
                gotToUpdateCheckpoint = TRUE;
            }
        } // End "AFTER UPDATE"
        
        return returnMe;
    }

    // ------- Methods, Static, Private -------
    
    @future
    private static void propagateDataIfRelevantFuture(Set<Id> relevantIds) {
        propagateDataIfRelevant(relevantIds);
    }

    private static void propagateDataIfRelevant(Set<Id> relevantIds) {
        firstproject_CustomObject dataPropagater = new firstproject_CustomObject(relevantIds); // Create a "computational worker"
        SimpleSObjectUpdater sObjUpdater = new SimpleSObjectUpdater(); // Create a "DML helper"
        dataPropagater.propagateData(sObjUpdater); // Delegate computational work and "pre-DML" caching
        // Do DML
        try {
            throwFakeExceptionIfAsked();
            sObjUpdater.updatePreDeclaredObjectsAndClearThemFromTypeCache(Custom_Object__c.SObjectType); // Do DML
        }
        catch ( DmlException e ) { 
            handleDmlException(e, relevantIds);
        }
    } // End propagateDataIfRelevant()

    private static void throwFakeExceptionIfAsked() {
        if ( throwFakeRecordLockException && Test.isRunningTest() ) { 
            // Make sure this block comes BEFORE the call to do DML so that 
            // unit tests can be sure the DML is really coming from the platform event trigger handler,
            // not this code.
            DmlException e = new DmlException();
            e.setMessage(RECORDLOCKMESSAGE);
            throw e;
        }
    } // End throwFakeExceptionIfAsked()

    private static void handleDmlException( DmlException e, Set<Id> idsToReprocess ) {
        // Don't bother with retry if some other error
        if ( !e.getMessage().contains(RECORDLOCKMESSAGE) ) { throw e; }
        // Try again in a little while, using the trigger I wrote against "FirstProject_Record_Lock_Event__e" platform events
        FirstProject_Record_Lock_Event__e platfEvt = new FirstProject_Record_Lock_Event__e( Record_IDs_To_Reprocess__c = JSON.serialize(idsToReprocess) );
        EventBus.publish(platfEvt);
    } // End handleDmlException()

}

And the unit tests to make sure they execute look something like this:

@isTest
private class zerothproject_TDTM_CustomObject_TEST {

    @TestSetup private static void insertTriggerHandler() {
        List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
        tokens.add(new hed.TDTM_Global_API.TdtmToken('zerothproject_TDTM_CustomObject', 'Custom_Object__c', 'BeforeInsert;BeforeUpdate', 1.00));
        hed.TDTM_Global_API.setTdtmConfig(tokens);
    }

    private static testMethod void testTriggerHandlerVariousFunctions() {
        Geographic_Area__c geoAreaNationBR = new Geographic_Area__c(ID_Code__c = 'NATION-Brazil', Name = 'NATION-Brazil');
        INSERT new List<Geographic_Area__c>{geoAreaNationBR}; {}
        
        Custom_Object__c co1 = new Custom_Object__c(City__c='Sao Paulo', State__c='XX', Zip__c='08940-000', Country__c='Brazil');
        Custom_Object__c co2 = new Custom_Object__c();
        
        Test.startTest();
        INSERT new List<Custom_Object__c>{co1, co2}; {}
        Custom_Object__c co2AfterDummyCheck = getFreshSOQL(co2.Id);
        System.assertEquals(NULL, co2AfterDummyCheck.Address_Geographic_Area__c, 'co2 Address_Geographic_Area__c started wrong');
        System.assertEquals(FALSE, co2AfterDummyCheck.ProcessZero_Involved_Field_Changed__c, 'co2 ProcessZero_Involved_Field_Changed__c started wrong');
        co2.City__c='Sao Paulo';
        co2.State__c='XX';
        co2.Zip__c='08940-000';
        co2.Country__c='Brazil';
        UPDATE new List<Custom_Object__c>{co2}; {}
        Test.stopTest();
        
        Custom_Object__c co1After = getFreshSOQL(co1.Id);
        System.assertEquals(geoAreaNationBR.Id, co1After.Address_Geographic_Area__c, 'co1 Address_Geographic_Area__c did not set correctly');
        System.assertEquals(FALSE, co1After.ProcessZero_Involved_Field_Changed__c, 'co1 ProcessZero_Involved_Field_Changed__c set but shouldn\'t have');

        Custom_Object__c co2After = getFreshSOQL(co2.Id);
        System.assertEquals(geoAreaNationBR.Id, co2After.Address_Geographic_Area__c, 'co2 Address_Geographic_Area__c did not set correctly');
        System.assertEquals(TRUE, co2After.ProcessZero_Involved_Field_Changed__c, 'co2 ProcessZero_Involved_Field_Changed__c did not set correctly');
    }

    private static Custom_Object__c getFreshSOQL(Id coID) {
        return [SELECT Address_Geographic_Area__c, ProcessZero_Involved_Field_Changed__c FROM Custom_Object__c WHERE Id = :coID];
    }
    
}

or like this:

@isTest
public class firstproject_TDTM_CustomObject_TEST {
    
    static testMethod void runTest() {
        
        Custom_Object__c co1 = new Custom_Object__c(Custom_Field_1__c='hello');
        INSERT co1;

        System.assertEquals(FALSE, firstproject_TDTM_CustomObject.gotToInsertCheckpoint);
        System.assertEquals(FALSE, firstproject_TDTM_CustomObject.gotToUpdateCheckpoint);
        
        // Retrieve default TDTM trigger handlers
        List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
        // Create our TDTM trigger handler using the constructor
        tokens.add(new hed.TDTM_Global_API.TdtmToken('firstproject_TDTM_CustomObject', 'Custom_Object__c', 'AfterInsert;AfterUpdate', 1.00));
        // Pass TDTM trigger handler config to set method for this test run
        hed.TDTM_Global_API.setTdtmConfig(tokens);

        Custom_Object__c co2 = new Custom_Object__c(Custom_Field_1__c='hi');
        INSERT co2;
        UPDATE co1;
        System.assertEquals(TRUE, firstproject_TDTM_CustomObject.gotToInsertCheckpoint, 'Insert failure');
        // We shouldn't reach our "checkpoint" if nothing interesting about co1 changed, so this should still be false
        System.assertEquals(FALSE, firstproject_TDTM_CustomObject.gotToUpdateCheckpoint, 'Update ran but should not have yet');

        co1.Custom_Field_2__c = 'World';
        UPDATE co1;
        System.assertEquals(TRUE, firstproject_TDTM_CustomObject.gotToInsertCheckpoint, 'Insert failure -- should still be TRUE');
        System.assertEquals(TRUE, firstproject_TDTM_CustomObject.gotToUpdateCheckpoint, 'Update failure, co1 update 1 of 2');
        
        firstproject_TDTM_CustomObject.gotToUpdateCheckpoint = FALSE;
        System.assertEquals(FALSE, firstproject_TDTM_CustomObject.gotToUpdateCheckpoint, 'Update checkpoint marker did not reset');
        
        // We shouldn't reach the "update checkpoint" for co1 again because we've already set co1 aside for future-processing once, which means we short-circuit doing it again.
        co1.Custom_Field_2__c = '!';
        UPDATE co1;
        System.assertEquals(FALSE, firstproject_TDTM_CustomObject.gotToUpdateCheckpoint, 'Update failure, co1 update 2 of 2');
    }
}

Complex business logic lives in yet another class

The “trigger handler” class I posted above follow Andy Fawcett’s principle from his book that business logic should be encapsulated independent of how it’s invoked.

Therefore, all of the actual business logic of what is supposed to happen when a Custom_Object__c’s trigger fires is in a totally different class, not shown here, called firstproject_CustomObject. That class, in turn, has its own firstproject_CustomObject_TEST unit test, also not shown here, that validates that firstproject_CustomObject.propagateData() alters the values of SObjects in memory appropriately.

Interestingly, firstproject_CustomObject.propagateData() doesn’t do any DML – that’s what SimpleSObjectUpdater.updatePreDeclaredObjectsAndClearThemFromTypeCache() does. SimpleSObjectUpdater is modeled after code Dan Appleman revealed on Pluralsight in “Adopting Trigger Design Patterns in Existing Salesforce Orgs” and in his book “Advanced Apex Programming”.

Andy’s principle that I shouldn’t let my “what’s supposed to happen when the trigger runs” logic be in the same codebase as the “make the trigger run” logic was really handy when the suite of firstproject-related triggers ended up stepping on each others’ toes, locking up records.

Because firstproject_CustomObject.propagateData() didn’t care how it got invoked, and because it wasn’t responsible for actually editing the database, I wrote a totally different Apex class that also invoked it, called firstproject_Batch_Reprocess_ById.

  1. That class, in turn, was used by firstproject_TDTM_RecLockPltfEvt, a trigger handler that runs after FirstProject_Record_Lock_Event__e inserts.
    • This helps re-try firstproject_CustomObject.propagateData() a minute or so after things have cooled off if the original trigger handler against Custom_Object__c didn’t go smoothly due to locked records.
  2. My “batch reprocess” class can also be used in anonymous apex against an arbitrary SOQL query of Custom_Object__c records.
    • This is handy for backfilling old data if there are changes to the business logic of what is supposed to happen when Custom_Object__c records are inserted or updated – that is, if the codebase of firstproject_CustomObject.propagateData() changes.

I use selectors more

I don’t know why I didn’t start doing this kind of thing way earlier.

Here’s the first bit of code I wrote against a greenfield scratch org that had EASY (Enterprise Application Solution for Yield) as a dependency:

public class firstproject_ProgramSelector {
    public static FINAL Schema.SObjectField level_field = Schema.Program__c.Career__c;
    public static FINAL String level_ug = 'Undergraduate';
    public static FINAL String level_grad = 'Graduate';

    private static Map<Id, Program__c> programsById;
    private static Boolean soqlWasEmpty = FALSE;

    // ------- Public methods -------

    public static Map<Id, Program__c> getAllProgramsById() {
        if ( programsById == NULL ) { 
            programsById = new Map<Id, Program__c>();
            queryProgramsById();
        } else if ( programsById.isEmpty() && Test.isRunningTest() ) {
            queryProgramsById();
        }
        return programsById;
    }

    public static Program__c getProgramById(Id programId) {
        if ( programsById == NULL || programsById.isEmpty() ) {
            getAllProgramsById();
        }
        return programsById.get(programId);
    }


    // ------- Private "helper" methods -------

    private static void queryProgramsById() {
        programsById = new Map<Id, Program__c>(
            [
                SELECT
                Id
                , Name
                , Career__c
                FROM Program__c
            ]
        );
        // Program__c is a validation table -- it should always have data -- someone probably
        // ran this too soon in an ISTest context and could use a chance to run it again.
        if ( programsById.isEmpty() && Test.isRunningTest() ) { soqlWasEmpty = TRUE; } // Flip switch on if necessary
        if ( soqlWasEmpty && Test.isRunningTest() && !programsById.isEmpty() ) { soqlWasEmpty = FALSE; } // Flip switch back off if now all okay
    }

}

And its unit test:

@isTest
public class firstproject_ProgramSelector_TEST {
    private static Program__c aProgram;
    private static Map<Id, Program__c> programsAfter;

    public static testMethod void test1() {
        programsAfter = firstproject_ProgramSelector.getAllProgramsById();
        System.assertEquals(0, programsAfter.size());

        Test.startTest();
        insertProgram();
        Test.stopTest();

        programsAfter = firstproject_ProgramSelector.getAllProgramsById();
        System.assertNotEquals(0, programsAfter.size());
        // Validate that there's data as expected in the "all programs by ID" function
        System.assertEquals(aProgram.Id, programsAfter.get(aProgram.Id).Id);
        // Validate that there's data as expected in the "single program by ID" function
        System.assertEquals(firstproject_ProgramSelector.level_ug, firstproject_ProgramSelector.getProgramById(aProgram.Id).Career__c);
    }

    public static testMethod void test2() {
        Test.startTest();
        insertProgram();
        Test.stopTest();

        // Validate that there's data as expected in the "single program by ID" function even though no "firstproject_ProgramSelector" functions have been called yet
        System.assertEquals(firstproject_ProgramSelector.level_ug, firstproject_ProgramSelector.getProgramById(aProgram.Id).Career__c);

    }

    // ---------------------------------

    private static void insertProgram() {
        aProgram = new Program__c(
            Career__c=firstproject_ProgramSelector.level_ug
        );
        INSERT aProgram;
    }

}

These selectors get particularly handy when the validation table in question is built with has a required unique external ID that means something – like when I built a custom object called Geographic_Area__c that indicates which Salesforce users handle which parts of the globe. Then I can add a getByExtlId() method.

Now I can write a one-liner every time I want the last name of the person in charge of Oregon, and it doesn’t re-query the database:

String or_lname = firstproject_GeoAreaSelector.getByExtlId('STATE-OR').Assigned_User__r.LastName;

(That is, as long as the SOQL query in the firstproject_GeoAreaSelector class makes sure to grab Assigned_User__r.LastName in its SELECT.)

I don’t always use selectors like this in my Apex code. There’s an ultra-large validation table of every high school and college in the nation, and I built out a selector for it so I could quickly grab details like firstproject_SchoolSelector.getByExtlId('CEEB-123456').Assigned_User__r.LastName in unit tests for System.AssertEquals() clauses.

However, I knew that querying thousands of schools just to get the Assigned_User__c in the trigger logic itself would be inefficient. Therefore, I have methods like getByExtlId() on firstproject_SchoolSelector marked as @TestVisible private so that they only get executed in unit tests contexts when the School__c object is nearly empty (other than records created specifically for the unit test on purpose).

In the actual trigger logic, I make sure that the constructor for the firstproject_CustomObject class, which freshly queries Custom_Object__c records for the IDs it’s invoked with, has a SELECT that includes fetching Current_School__r.Assigned_User__r.LastName fields and whatever else I might need to know to get the business logic done.

But since Geographic_Area__c is a relatively small table, and since its selector does “lazy SOQL” that doesn’t run until the first time data is requested from firstproject_GeoAreaSelector, and since Custom_Object__c doesn’t have any fields that explicitly look up to Geographic_Area__c records, I’ve found that my code is nice and clean with such a class.

I even gave it a little utility method, suggestGeoAreaFromAddress(), so I can just run this kind of code when I need it:

Custom_Object__c co = [SELECT Id, City__c, State__c, Zip__c, Country__c FROM Custom_Object__c LIMIT 1];
System.debug('Last name of user assigned by custom object address is ' + firstproject_GeoAreaSelector.suggestGeoAreaFromAddress(co.City__c, co.State__c, co.Zip__c, co.Country__c)?.Assigned_User__r?.LastName);
// If I need to run "suggestGeoAreaFromAddress()" again, it will run faster because all Geographic_Area__c records are now in memory.

The codebase behind the magic is:

public class firstproject_GeoAreaSelector {
    private static Set<String> oktxStateStrings = new Set<String>{'OK','TX'}; {}

    private static Map<Id, Geographic_Area__c> geoAreasById;
    private static Boolean soqlWasEmpty = FALSE;
    private static Map<String, Geographic_Area__c> geoAreasByExtlId;

    // ------- Public methods -------
    
    public static Map<Id, Geographic_Area__c> getAllGeoAreasById() {
        if ( geoAreasById == NULL ) { 
            geoAreasById = new Map<Id, Geographic_Area__c>();
            queryGeoAreasById();
        } else if ( geoAreasById.isEmpty() && Test.isRunningTest() ) {
            queryGeoAreasById();
        }
        return geoAreasById;
    }

    public static Map<String, Geographic_Area__c> getAllGeoAreasByExtlId() {
        if ( geoAreasByExtlId == NULL ) {
            geoAreasByExtlId = new Map<String, Geographic_Area__c>();
        }
        if ( geoAreasByExtlId.isEmpty() ) {
            getAllGeoAreasById();
            for ( Geographic_Area__c geoArea : geoAreasById.values() ) { 
                if ( geoArea.Id_Code__c == NULL ) { continue; }
                geoAreasByExtlId.put(geoArea.Id_Code__c, geoArea);
            }
        }
        return geoAreasByExtlId;
    }

    public static Geographic_Area__c getGeoAreaById(Id geoAreaId) {
        if ( geoAreasById == NULL || geoAreasById.isEmpty() ) {
            getAllGeoAreasById();
        }
        return geoAreasById.get(geoAreaId);
    }

    public static Geographic_Area__c getGeoAreaByExtlId(String externalId) {
        if ( geoAreasByExtlId == NULL || geoAreasByExtlId.isEmpty() ) {
            getAllGeoAreasByExtlId();
        }
        return geoAreasByExtlId.get(externalId);
    }

    // More of a utility method against Geographic_Area__c than a selector, but I kind of like it here.
    public static Geographic_Area__c suggestGeoAreaFromAddress(String city, String state, String zip, String country) {
        Geographic_Area__c returnMe = NULL;
        String geoAreaKeyIdea;
        // Try by nearby city
        if ( oktxStateStrings.contains(state) && city <> NULL ) {
            geoAreaKeyIdea = 'CITY-'+(city+state).toLowerCase().replaceAll('[^a-zA-Z]', '');
            returnMe = getGeoAreaByExtlId(geoAreaKeyIdea);
            if ( returnMe != NULL ) { return returnMe; }
        }
        // Try by nearby zip
        if ( oktxStateStrings.contains(state) && city <> NULL ) {
            geoAreaKeyIdea = 'ZIP-'+zip;
            returnMe = getGeoAreaByExtlId(geoAreaKeyIdea);
            if ( returnMe != NULL ) { return returnMe; }
        }
        // Try by faraway state
        if ( !String.isBlank(state) ) {
            geoAreaKeyIdea = 'STATE-'+state;
            returnMe = getGeoAreaByExtlId(geoAreaKeyIdea);
            if ( returnMe != NULL ) { return returnMe; }
        }
        // Try by country
        if ( !String.isBlank(country) ) {
            geoAreaKeyIdea = 'NATION-'+country;
            returnMe = getGeoAreaByExtlId(geoAreaKeyIdea);
            if ( returnMe != NULL ) { return returnMe; }
        }
        return returnMe;
    }


    // ------- Private "helper" methods -------
    
    private static void queryGeoAreasById() {
        geoAreasById = new Map<Id, Geographic_Area__c>(
            [
                SELECT
                Id
                , Id_Code__c
                , Assigned_User__c
                , Assigned_User__r.LastName
                FROM Geographic_Area__c
            ]
        );
        // Geographic_Area__c is a validation table -- it should always have data -- someone probably
        // ran this too soon in an ISTest context and could use a chance to run it again.
        if ( geoAreasById.isEmpty() && Test.isRunningTest() ) { soqlWasEmpty = TRUE; } // Flip switch on if necessary
        if ( soqlWasEmpty && Test.isRunningTest() && !geoAreasById.isEmpty() ) { soqlWasEmpty = FALSE; } // Flip switch back off if now all okay
    }

}

With a unit test like this:

@isTest
public class firstproject_GeoAreaSelector_TEST {
    User u = [SELECT Id FROM User WHERE LastName='Smith' LIMIT 1];
    private static Geographic_Area__c aGeoArea;
    private static Map<Id, Geographic_Area__c> geoAreasAfter;
    
    public static testMethod void test1() {
        geoAreasAfter = firstproject_GeoAreaSelector.getAllGeoAreasById();
        System.assertEquals(0, geoAreasAfter.size());
        
        Test.startTest();
        insertGeoArea();
        Test.stopTest();
        
        geoAreasAfter = firstproject_GeoAreaSelector.getAllGeoAreasById();
        System.assertNotEquals(0, geoAreasAfter.size());
        // Validate that there's data as expected in the "all geoAreas by ID" function
        System.assertEquals(aGeoArea.Id_Code__c, geoAreasAfter.get(aGeoArea.Id).Id_Code__c);
        // Validate that there's data as expected in the "single geoArea by ID" function
        System.assertEquals(u.Id, firstproject_GeoAreaSelector.getGeoAreaById(aGeoArea.Id).Assigned_User__c);
        // Validate that there's a key as expected in the "all geoAreas by external ID" function
        System.assert(firstproject_GeoAreaSelector.getAllGeoAreasByExtlId().containsKey('NATION-Brazil'));
        // Validate that there's data as expected in the "single geoArea by external ID" function
        System.assertEquals('Smith', firstproject_GeoAreaSelector.getGeoAreaByExtlId(aGeoArea.Id_Code__c).Assigned_User__r.LastName);
    }
    
    public static testMethod void test2() {
        Test.startTest();
        insertGeoArea();
        Test.stopTest();
        
        // Validate that there's data as expected in the "single geoArea by unique ID" function even though no "firstproject_GeoAreaSelector" functions have been called yet
        System.assertEquals('Smith', firstproject_GeoAreaSelector.getGeoAreaByExtlId(aGeoArea.Id_Code__c).Assigned_User__r.LastName);
    }
    
    private static testMethod void testMailingAddressesBecomeGeoAreasCorrectly() {
        // Set up baseline data
        Geographic_Area__c geoAreaNationBrazil = new Geographic_Area__c(ID_Code__c='NATION-Brazil', Name='NATION-China (CH)', Assigned_User__c = u.Id);
        Geographic_Area__c geoAreaStateKY = new Geographic_Area__c(ID_Code__c = 'STATE-KY', Name = 'STATE-KY', Assigned_User__c = u.Id);
        Geographic_Area__c geoAreaZip75555 = new Geographic_Area__c(ID_Code__c = 'ZIP-75555', Name = 'ZIP-75555', Assigned_User__c = u.Id);
        Geographic_Area__c geoAreaZip75001 = new Geographic_Area__c(ID_Code__c = 'ZIP-75001', Name = 'ZIP-75001', Assigned_User__c = u.Id); // 75001 is in Dallas, but city trumps Zip
        Geographic_Area__c geoAreaCityDallas = new Geographic_Area__c(ID_Code__c = 'CITY-dallastx', Name = 'CITY-dallastx', Assigned_User__c = u.Id); // Contains 75001
        INSERT new List<Geographic_Area__c>{geoAreaNationBrazil, geoAreaStateKY, geoAreaZip75555, geoAreaZip75001, geoAreaCityDallas}; {}
        
        // Validate mailing address computation
        System.assertEquals(geoAreaNationBrazil.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('Manaus', 'Amazonas', NULL, 'Brazil')?.ID_Code__c);
        System.assertEquals(geoAreaStateKY.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('Lexington', 'KY', NULL, NULL)?.ID_Code__c);
        System.assertEquals(geoAreaZip75555.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('City not in the database', 'TX', '75555', NULL)?.ID_Code__c);
        System.assertEquals(geoAreaCityDallas.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('Dallas', 'TX', '75001', 'United States')?.ID_Code__c);
        System.assertEquals(geoAreaCityDallas.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('Dallas', 'TX', NULL, 'United States')?.ID_Code__c);
        System.assertEquals(geoAreaZip75001.ID_Code__c, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('City not in the database', 'TX', '75001', 'United States')?.ID_Code__c);
        System.assertEquals(NULL, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress(NULL, 'TX', NULL, 'United States')?.ID_Code__c);
        System.assertEquals(NULL, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress('Nonexistent City', 'TX', 'ABCDE', 'United States')?.ID_Code__c);
        System.assertEquals(NULL, firstproject_GeoAreaSelector.suggestGeoAreaFromAddress(NULL, NULL, NULL, NULL)?.ID_Code__c);
    }
    
    // ---------------------------------
    
    private static void insertGeoArea() {
        aGeoArea = new Geographic_Area__c(
            Id_Code__c='NATION-Brazil',
            Name='NATION-Brazil (BR)',
            Assigned_User__c=u.Id
        );
        INSERT aGeoArea;
    }
    
}

Modular code facilitates lift-and-shift

This whole trigger handler suite has to migrate from an org using EnrollmentRx to one using EASY.

One thing I’m really glad I did last time I refactored the unit tests was to make a lot of methods that seemed a bit silly:

private static void setFieldsToUndergrad(EnrollmentrxRx__Enrollment_Opportunity__c app) {
    app.Program__c = firstproject_ProgramSelector.getByExternalId('Undergraduate');
}

But when I refactor it, all I have to do is find every unit test in the EnrollmentRx codebase that includes this text:

EnrollmentrxRx__Enrollment_Opportunity__c test_app = new EnrollmentrxRx__Enrollment_Opportunity__c();
setFieldsToUndergrad(test_app);

And replace the definition of test_app so it’s an EASY application, not an EnrollmentRx application:

Application__c test_app = new Application__c();
setFieldsToUndergrad(test_app);

And make a new mini-method for EASY:

private static void setFieldsToUndergrad(Application__c app) {
    app.Intended_Program__c = firstproject_ProgramSelector.getByExternalId('Undergraduate');
}

I could, of course, instead have had a bunch of unit tests that simply read like this:

EnrollmentrxRx__Enrollment_Opportunity__c test_app = new EnrollmentrxRx__Enrollment_Opportunity__c();
test_app.Program__c = firstproject_ProgramSelector.getByExternalId('Undergraduate');

And found-and-replaced .Program__c = with .Intended_Program__c = :

Application__c test_app = new Application__c();
test_app.Intended_Program__c = firstproject_ProgramSelector.getByExternalId('Undergraduate');

But … I don’t know … somehow I just feel like I’m going to have fewer find-and-replace bugs, or catch them sooner, this way.

Actually, the biggest reason I did it is because find-and-replace might not be so simple in the org migration.

If setting an application to “undergraduate” actually comes to mean setting two fields, I’ll really be happy I made a setFieldsToUndergrad() helper method.

Then I can, for example, have the new helper for EASY look like this:

private static void setFieldsToUndergrad(Application__c app) {
    app.Intended_Program__c = firstproject_ProgramSelector.getByExternalId('Undergraduate');
    app.Application_Control__c = firstproject_AppCtrlSelector.getByExternalId('Undergraduate');
}

But my find-and-replace of EnrollmentrxRx__Enrollment_Opportunity__c with Application__c will stay simple.

Namespace code unless it is truly universal

Okay, so not literally namespaced as in packaged, but that whole firstproject_ thing I’m doing with naming classes? That’s definitely deliberate.

I used to put anything that would be needed by more than 2 Apex classes – like the street-address-to-geographic-area converter – into a single mega-class called Util.

Now I find that I can’t lift-and-shift this project without picking out only the pieces of Util that have to do with this project.

So I’m definitely going with having a whole lot of highly-specific classes – if I can’t come up with a good home for a method, at least putting it into a firstproject_Util class so that it’s not tangled with methods that support completely unrelated business logic.

I’m trying, as I refactor, to ensure that code found in a class with as universal of a name as Util is truly something I might want in any org, regardless of the project.

Here’s one:

public class DeveloperException extends Exception { private FINAL Boolean testCoverageTrick = FALSE; } // Exception representing a developer coding error, not intended for end user eyes

And its test:

public static testMethod void testDeveloperException() {
    try {
        throw new Util.DeveloperException('Hi there');
    }
    catch(Util.DeveloperException e) {
        System.assertEquals('Hi there', e.getMessage());
    }
}

Or this handy little thing for writing Apex that works with junction tables whose sole purpose is to master-detail two other tables together:

public class TwoIdWrapper {
    private final Id FIRSTID {get; private set;}
    private final Id SECONDID {get; private set;}
    private final Set<Id> IDSET;
    private final String OUTSTRING;
    
    public TwoIdWrapper() { this(NULL, NULL); }
    
    public TwoIdWrapper(Id first_id, Id second_id) {
        FIRSTID = first_id;
        SECONDID = second_id;
        IDSET = new Set<Id>{FIRSTID, SECONDID};
        OUTSTRING = ((String)FIRSTID>(String)SECONDID) ? (SECONDID+'|'+FIRSTID) : (FIRSTID+'|'+SECONDID);
    }
    
    public Integer hashCode() {
        return System.hashCode(IDSET);
    }
    
    public Boolean equals(Object other) {
        TwoIdWrapper theOther = (TwoIdWrapper)other;
        return System.equals(IDSET, theOther.IDSET);
    }

    public Override String toString() { 
        return OUTSTRING;
    }
    
}
public static testMethod void testTwoIdWrapper() {
    Id pId = userinfo.getProfileId();
    Id uId = userinfo.getOrganizationId();
    Id oId = userinfo.getUserId();
    
    Util.TwoIdWrapper puWrap = new Util.TwoIdWrapper(pId, uId);
    Util.TwoIdWrapper puWrapDupl = new Util.TwoIdWrapper(pId, uId);
    Util.TwoIdWrapper upWrap = new Util.TwoIdWrapper(uId, pId);
    Util.TwoIdWrapper poWrap = new Util.TwoIdWrapper(pId, oId);
    
    System.assertEquals(puWrap, puWrapDupl, 'pu and pudupl wraps failed to match');
    System.assertEquals(puWrap, upWrap, 'pu and up wraps failed to match');
    System.assertNotEquals(puWrap, poWrap, 'pu and po matched but should not');
    
    Set<Util.TwoIdWrapper> puWrapPuWrapDuplSet = new Set<Util.TwoIdWrapper>{puWrap, puWrapDupl};
    Set<Util.TwoIdWrapper> puWrapUpWrapSet = new Set<Util.TwoIdWrapper>{puWrap, upWrap};
    Set<Util.TwoIdWrapper> puWrapPoWrapSet = new Set<Util.TwoIdWrapper>{puWrap, poWrap};
    System.assertEquals(1, puWrapPuWrapDuplSet.size(), 'pu and pudupl wraps failed to match in set');
    System.assertEquals(1, puWrapUpWrapSet.size(), 'pu and up wraps failed to match in set');
    System.assertEquals(2, puWrapPoWrapSet.size(), 'pu and op wraps failed set size 2');

    System.assertEquals(puWrap.toString(), puWrapDupl.toString(), 'toString error - puduplWrap');
    System.assertEquals(puWrap.toString(), upWrap.toString(), 'toString error - upWrap');
    System.assertNotEquals(puWrap.toString(), poWrap.toString(), 'toString error - poWrap');
}
--- ---