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

Equals and HashCode in Salesforce Apex Classes and Objects

05 Sep 2019 🔖 salesforce tutorials apex
💬 EN

Table of Contents

In this post, we’ll look at overriding Salesforce Apex classes’ hashCode and equals functions to make objects instantiated from them serve as “data wrappers” than can be compared to each other for “redundancy” based on contents of our choosing (which makes them sort nicely into Sets).

Background

I’m reading Andy Fawcett’s Force.com Enterprise Architecture.

I’ve got some new business processes to write as triggers this month, so I’m thinking about “small wins” from the book that I can implement as I work.

My first thought was to refactor some of my “primitive obsessions” into objects.

Apparently I might just be going from one “code smell” to another (there’s an argument that it’s not ideal to do “pure data classes“), but nevertheless, I think this will help me move towards the kinds of principles I’m seeing in Andy’s suggestions.

Use Case: Caching Inserts

In loops that set aside SObjects for database record updates, it’s easy to avoid redundant / recursive processing.

I use a class-level set of IDs to keep track of records to skip should my business logic ever encounter them again. It looks something like this:

private static Set<Id> recordsIAlreadyInspectedThisExecutionContext;

But what do I do when I want to set aside SObjects for database record inserts?

What if this is my business logic?

  • If the Account in question is located in Wisconsin, USA …
  • … and if it has a non-null BillingCity
  • … and if it doesn’t already have such a record …
    • … then create 1 new child Contact record with first name matching the Account’s BillingCity and last name 'Packers-Superfan'.

If, for some reason, a given Account re-passes through my execution context, I don’t want to have to run an extra SOQL query to know that I’ve already “taken care of” the Packers Superfan requirement.

In the past, my loops have cross-checked values they find or propose instantiating against a class-level set of Strings something along the lines of:

private static Set<String> noMoreSuchContacts;

And then my code will involve a lot of concatenated-string values like this:

existingC.AccountId + '|' + existingC.FirstName + '|' + existingC.LastName`

Or this:

- `acct.Id` + '|' + acct.BillingCity + '|' + `Packers-Superfan`

Let’s be honest: it’s pretty awkward.

Any Contact SObject I find (from the one query I did to get existing child Contact records) or create myself already knows its AccountId, FirstName, and LastName.

Wouldn’t it be nice if I could just cross-check against a set of Contact records, more like this?

static Set<Contact> noMoreSuchContacts`;

Problem

[[TO DO: nevermind...I was wrong.]]

Unfortunately, there’s no way I know of to “trick” Apex into thinking, for the purposes of using Set, that mySet below should have just 1 member instead of 2:

@isTest
public class TestStuff {
	static testMethod void runTest() {
		Contact c1 = new Contact(FirstName='Katie', LastName='Kodes');
		Contact c2 = new Contact(FirstName='Katie', LastName='Kodes');
		System.assertEquals(c2, c1, 'Contacts not the same.');
		Set<Contact> mySet = new Set<Contact>{c1, c2}; {}
		System.assertEquals(1, mySet.size(), 'Size wrong.');
		Test.startTest();
		INSERT c1;
		Test.stopTest();
		// c1's hash should have changed to be based on its ID, 
		// rather than the values of its other fields, 
		// now that it has an ID,
		// meaning its hash will no longer match that of c2.
		mySet.add(c2);
		System.assertEquals(2, mySet.size(), 'Size wrong.');
	}
}

Solution

However, it is possible to make Apex think one of my own Apex-defined classes can behave that way!

It is quite possible to cause mySet below to have just 1 member if do this if I hand-define two methods on MyClass:

MyClass mc1 = new MyClass('Katie','Kodes');
MyClass mc1 = new MyClass('Katie','Kodes');
Set<MyClass> mySet = new Set<MyClass>{mc1, mc2};

The two methods I need to hand-define are .hashCode() and .equals().

Note: Technically, I’m overriding virtual methods inherited from the Apex type Object.

That’s how I can be sure that functions like Set.add() and System.assertEquals() will respect my definitions.

Yes, I’ll have to jump through an extra hoop of using instances of my custom wrapper class when dealing with noMoreSuchContacts – it’s not quite as tidy as, say, a Set<Contact> would be – but it still feels “tidier” than concatenated strings.

With an inner “data class” like this:

private class MyClass {
	private final Integer IDNUM {get; private set;}
	private final String FIRST_NAME;
	private final String LAST_NAME;
	private final String FULL_NAME {get; private set;}
	
	public MyClass() { this('Default First', 'Default Last'); }
	
	public MyClass(String fn, String ln) {
		IDNUM = Integer.valueof(Math.random() * 100000000);
		FIRST_NAME = fn;
		LAST_NAME = ln;
		FULL_NAME = fn + ln;
	}
	
	public Integer hashCode() {
		return System.hashCode(FULL_NAME);
	}
	
	public Boolean equals(Object other) {
		MyClass theOther = (MyClass)other;
		return System.equals(FULL_NAME, theOther.FULL_NAME);
	}
	
}

I can do this:

@isTest
public class TestStuff {
    static testMethod void myUnitTest() {
        
        MyClass mc1 = new MyClass('AAA','AAA');
        MyClass mc2 = new MyClass('BBB','BBB');
        MyClass mc3 = new MyClass('AAA','AAA');
        
        // Prove that mc1 and mc3 are separate Apex objects with separate "IDNUM" values
        System.assertNotEquals(mc3.IDNUM, mc1.IDNUM);
        // Prove that mc1 and mc3 "equals()" each other nonetheless due to our override of ".equals()" and ".hashCode()"
        System.assertEquals(mc3, mc1);

        // Prove that when we put all 3 "mcs" in a set, the set length is only 2, because mc1 and mc3 were "the same" due to our override of ".hashCode()"
        Set<MyClass> mySet = new Set<MyClass>{mc1, mc2, mc3};
        mySet.add(mc3);
        System.assertEquals(2, mySet.size());
        
        // Demonstrate that we need to be careful -- the first Apex object we added to the set for a given ".hashCode()" is going to be what's ACTUALLY in there!
        System.assert(mySet.contains(mc1), 'mc1 not present in set');
        System.assert(mySet.contains(mc3), 'mc3 not present in set');
        Set<MyClass> mySetClone = mySet.clone();
        mySetClone.remove(mc2);
        MyClass survivingMC = (new List<MyClass>(mySetClone))[0];
        System.assertEquals(mc1.IDNUM, survivingMC.IDNUM, 'survivingMC is not mc1 IDNUM');
        System.assertNotEquals(mc3.IDNUM, survivingMC.IDNUM, 'survivingMC is mc3 IDNUM but should not be'); // <---- SEE HERE
    }
}

Notes

Check out the comments and the assertions in the code above.

Note that although when we add 3 MyClass records to a set, only 2 of them actually get added because the first and third “look alike” in terms of name.

They are still separate objects in memory, which we can prove with System.assertNotEquals(comp3.IDNUM, comp1.IDNUM);.

  • Q: That begs the question … which one actually made it into mySet? mc1 or mc3?
  • A: mc1. I tried this with both new Set<MyClass>{mc1, mc2, mc3} and with new Set<MyClass>{mc1, mc2} plus mySet.add(mc3) and the “member of the set that wasn’t mc2” always had an ID number matching that of mc1.
    • Looks like the first object into a set plants itself firmly there and stays put.

Why Not Maps?

  • Q: Would I bother going this far if I were banging out a quick script in a language besides Apex?
  • A: Maybe not. It’s just so easy in concise, dynamically typed languages like Python to throw a hodge-podge of primitives into a set of tuples or set of dicts and check for “in”.

I mean, I could do this in Apex:

@isTest
public class TestStuff {
    static testMethod void runTest() {
        Map<String, String> mp1 = new Map<String, String>{ 'fn' => 'Katie' , 'ln' => 'Kodes' };
        Map<String, String> mp2 = new Map<String, String>{ 'fn' => 'Katie' , 'ln' => 'Kodes' };
        System.assertEquals(mp2, mp1, 'Maps not the same');
        Set<Map<String, String>> mySet = new Set<Map<String, String>>{mp1, mp2};
        System.assertEquals(1, mySet.size(), 'Wrong set size');
    }
}

Which is about equivalent to this in Python:

from collections import namedtuple
ContactSeen = namedtuple('ContactSeen', ['fn','ln'])
mp1 = ContactSeen(fn='Katie',ln='Kodes')
mp2 = ContactSeen(fn='Katie',ln='Kodes')
assert mp1 == mp2, 'Maps not the same'
mySet = {mp1, mp2}
assert len(mySet) == 1, 'Wrong set size'

Or this, if it’s a short script and I can be strict about remembering the order of values in my tuple on my own:

mp1 = ('Katie','Kodes')
mp2 = ('Katie','Kodes')
assert mp1 == mp2, 'Maps not the same'
mySet = {mp1, mp2}
assert len(mySet) == 1, 'Wrong set size'

In a dynamically typed, concise-syntax language like Python, tuples are a pretty tempting alternative to string concatenation – even though they’re sort of just a souped-up form of string concatenation or, in the case of named tuples, maps.

They’re “souped up” in that I’m not limited to String-typed data as values … I can throw in a few dates, integers, etc. for good measure.

Apex, though, is syntax-heavy, and it requires me to keep converting things into strings and back (since I restricted ALL the map values to be strings – no dates or integers allowed!).

Personally, I find the Map approach more awkward than string concatenation, not lighter-weight the way I would in Python with tuples.

--- ---