23

In Dan Appleman's book Advanced Apex Programming, he explains how using SObjects as Map keys (i.e. Map<SObject, *value*>) is supported but it's poor practice, since the key is actually determined by a hash of all the fields on that SObject key - so if there's a change to even just one field, the key is changed.

My question is - why is this supported? I'm having trouble thinking of even one case where using this type of collection would be desired? The point of using a Map is to be able to retrieve data based on semantic-trivial, immutable data (such as IDs)...

  • 1
    Every sObject will have an Id as the first field. I was just helping someone the other day where they were updating multiple fields inside a wrapper class (a loop within a loop). Getting those records from the innermost loop to outside the outer loop was a hassle since different fields were being changed on each iteration and saved to the sObject. An sObject map would have been one solution. – crmprogdev Apr 7 '15 at 15:34
  • 99% of the time, probably better to use Id if you have it. Mapping records that have not been inserted is a bit more tricky depending on what contextual information is available. – Adrian Larson Apr 7 '15 at 16:02
  • Related: Using an sObject as a Map key – Daniel Ballinger Sep 24 '15 at 2:23
24

I use SObject as a key when I want to use a composite key. For example, say I want to find duplicates based on first name, last name, and email. I might loop through a list of contacts, and create a contact that matches the keys I'm using:

Incoming.put(new Contact(FirstName= record.FirstName, LastName= record.LastName, Email= record.Email), record);

Then, when I query the database on those three fields, I can quickly look up the new records, again by constructing a key from an SObject.

This is more reliable than concatenating strings and hoping that it is unique enough that it won't grab false positives.


Example:

trigger matchDupes on Lead (before insert, before update) {
    Map<Lead, Lead> leads = new Map<Lead, Lead>();
    Set<String> company = new Set<String>(), email = new Set<String>();

    // Match duplicates in this transaction
    for(Lead record: Trigger.new) {
        Lead key = new Lead(Company=record.Company, Email=record.Email);
        if(leads.containsKey(key)) {
            record.addError('This lead matches a duplicate lead.');
            continue;
        }
        company.add(record.company);
        email.add(record.email);
        leads.put(key, record);
    }
    // Match duplicates in the database
    for(Lead record: [SELECT Email, Company from Lead WHERE Company in :company and Email IN :email]) {
        Lead key = new Lead(Company=record.Company, Email=record.Email),
             dupLead = leads.remove(key);
        if(dupLead != null) {
           dupLead.addError('This lead matches a duplicate lead.');
        }
    }
}

Note that I could use an arbitrary number of fields (in most cases). There's several considerations, such as case sensitivity, not included here. I'd recommend always trying to convert to a common case when putting into/out of the map, but generally speaking this technique works well.

  • 1
    The SObject construct appears to use less CPU time than using strings, at least for a two key composite. However, the benefit is only about 5%. I'll have to do more extensive testing later. – sfdcfox Apr 7 '15 at 19:27
  • @sfdcfox, could you elaborate more on this approach, perhaps more code samples? I want de-dupe large list of records and looks like this could be a good approach. – dzh Apr 10 '15 at 1:06
  • 2
    @dzh Ask, and you shall receive. – sfdcfox Apr 10 '15 at 19:40
10

Sfdcfox explains a great reason to use this, but at a more abstract level the reason it's supported it because maps can have any type as a key.

In a model where custom classes, or any other type, can be used as keys there seems little reason to make a special case for forbidding SObjects.

Using an apex class as a map key has equally odd behavior (without implementing the equals and hashCode methods at least), so by this logic making SObject special seems inexplicable.

  • 2
    Any type except no type at all, anyways: Map<Void, Object> fails to compile with a particularly specific error: "Map by void not allowed" but Map<Object, void> is allowed. – sfdcfox Apr 10 '15 at 19:45
6

Here's another use-case:

I have an algo that creates Opps and Opportunity Lineitems , then rolls up the Opportunity Lineitems based on part product-code, then inserts Opps, then finally the OLIs.

// helper collection needed for roll-up routine
Map<OpportunityLineItem, String> opportunityLineItemToProductCodeMap = new Map<OpportunityLineItem, String>();

This is implemented with a deferred DML chain (for bulkification), so neither the OLIs nor the Opportunities have Ids until after the roll-up.

When I assign a part (using PricebookEntryId) to the Opportunity Lineitem, I found that I could not reference through to the part's Sku from the Oli object (because it's uncommitted), so I had to maintain a data structure that contains the Oli to product code relationship purely for the roll-up exercise.

Why I Think It's Okay In This Case:

Because 2 identical lineitems would map to the same product code, I don't need to worry about the case where identical field values result in an overwrite of the product code (it would be the same). I also guarantee that once the supporting data structure is in use, the keyed OLIs never change until after the data structure is utilized.

Note that this wouldn't be necessary if my requirement was to roll-up based on part-Id rather than product code. A part-Id based roll-up would require that product code is unique across all parts, and I have duplicates in my data set. Instead, I am able to roll-up on product code only because I can assert that all parts with duplicate product codes are interchangeable (from my stakeholder's perspective) and the price is not prescribed by what might be a variable list price, but rather by the opportunity line item unit price itself, which is being calculated independent of list price.

Alternatively...

However, the above is probably less readable and maintainable than a inner-class holding both the OLi and the product code String, which could then be stored together in a single list.

Similarly, one could use two lists and rely on the ordinality of lists to extract the elements from both while looping through only 1 (the OLI list).

  • 1
    Sorry I downvoted this by accident while on my tablet :( – Girbot May 27 '15 at 10:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.