29

Are sObjects supported as the keys for Maps?

I thought they were, but it appears that any subsequent changes to the key sObject break the mapping to the value.

This might be better shown with an anonymous apex example (API v27.0).

Map<OpportunityLineItem, string> testMap = new Map<OpportunityLineItem, string>();
OpportunityLineItem testOli = new OpportunityLineItem();
testOli.Description = 'foo';
testMap.put(testOli, 'bar');
System.assert(testMap.containsKey(testOli));
string mapValue = testMap.get(testOli);
System.assertEquals('bar', mapValue);

// After this the OLI will no longer be a key in the map.
testOli.OpportunityId = '0064000000RB2eJ';

System.assert(testMap.containsKey(testOli), 'Expected to still contain OLI');
string mapValue2 = testMap.get(testOli);
System.assertEquals('bar', mapValue2, 'OLI instance still expected to map to string bar');

This fails for me with:

System.AssertException: Assertion Failed: Expected to still contain OLI

Is this the expected behaviour?

3 Answers 3

27

Yes, that is the expected behavior. Behind the scenes, Salesforce is hashing the sObject. By changing field values, you change the hash. A better practice is to use the sObject ID as the key. This blog post explains some more: https://appirio.com/tech-blog/considerations-using-sobjects-in-sets-and-map-keys-in-apex

When planning to use an SObject as the key for a Map, be aware that it can sometimes behavein an unexpected fashion. If you are expecting to encounter a scenario where the SObject can be mutated (even indirectly, as done by an insert) once set as a key for this Map, you will lose the reference to the value for this Map for the SObject key that was mutated.

2
  • The hashing makes sense, as the Map still has the correct size, but couldn't find the updated sObject. Commented Jun 5, 2013 at 4:19
  • 2
    Link is broken, unfortunately. Commented Aug 31, 2016 at 7:12
12

It appears it is the expected behaviour.

Uniqueness of keys of all other non-primitive types, such as sObject keys, is determined by comparing the objects’ field values.

Be cautious when using sObjects as map keys. Key matching for sObjects is based on the comparison of all sObject field values. If one or more field values change after adding an sObject to the map, attempting to retrieve this sObject from the map returns null. This is because the modified sObject isn’t found in the map due to different field values. This can occur if you explicitly change a field on the sObject, or if the sObject fields are implicitly changed by the system; for example, after inserting an sObject, the sObject variable has the ID field autofilled. Attempting to fetch this Object from a map to which it was added before the insert operation won’t yield the map entry, as shown in this example.

Source - Map Considerations

0

I know this is pretty old, but I've been trying to figure out a way to deal with this recently and what I've found out is that, while SObjects will change its hash code whenever it gets modified, non SObjects will work even if you modify its properties after.

So to get around the unpredictability of SObjects as key in maps, I ended up simply wrapping the SObjects, such as Account/CustomObj__c, in a 'container' class:

public class AccountContainer {
    public Account accountSObject;
    public AccountContainer(Account accountSObject) { this.accountSObject = accountSObject; }
}
public class ContactContainer {
    public Contact contactSObject;
    public ContactContainer(Contact contactSObject) { this.contactSObject = contactSObject; }
}

Account a1 = new Account(Name = 'Account 1');
AccountContainer ac1 = new AccountContainer(a1);
Contact c1 = new Contact(LastName = 'Contact 1');
ContactContainer cc1 = new ContactContainer(c1);

Map<ContactContainer, AccountContainer> mapAccountByContact_Container = new Map<ContactContainer, AccountContainer>{ cc1 => ac1 };
Map<Contact, Account> mapAccountByContact_SObj = new Map<Contact, Account>{ c1 => a1};

AccountContainer beforeChanging_Container = mapAccountByContact_Container.get(cc1);
Account beforeChanging_SObject = mapAccountByContact_SObj.get(c1);
System.debug(beforeChanging_SObject === a1);        // true
System.debug(beforeChanging_Container === ac1);     // true

c1.LastName = 'Contact 1 changed';
AccountContainer afterChanging_Container = mapAccountByContact_Container.get(cc1);
Account afterChanging_SObject = mapAccountByContact_SObj.get(c1);
System.debug(beforeChanging_SObject === afterChanging_SObject);         // false
System.debug(beforeChanging_Container === afterChanging_Container);     // true

Not sure if it solves all cases of SObjects as map key, or if it has significant flaws (if any), but worked perfectly for my particular case.

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .