2

I frequently find I want/need to map a list of SObjects by a value other than it's own Id.

So, I'd like to abstract this process to make it reusable.

For example, I might want to map Contacts by Account Id.

So I've created a method mapBySpecifiedIdField:

    public static Map<Id, Set<SObject>> mapBySpecifiedIdField (List<SObject> homogeneousSObjectList, SObjectField idField) {
    if (homogeneousSObjectList == null || homogeneousSObjectList.isEmpty()) {
        return null;
    }

    Map<Id, Set<SObject>> sObjectSetBySpecifiedIdMap = new Map<Id, Set<SObject>>();
    for (SObject sObj : homogeneousSObjectList) {
        Id specifiedId = (Id) sObj.get(idField);
        if (!String.isBlank(specifiedId)) {
            if (!sObjectSetBySpecifiedIdMap.containsKey(sObj.Id)) {
                sObjectSetBySpecifiedIdMap.put(sObj.Id, new Set<SObject>());
            }
            sObjectSetBySpecifiedIdMap.get(sObj.id).add(sObj); 
        }
    }

    String mapType = 'Map<Id, Set<' + homogeneousSObjectList[0].getSObjectType() + '>>';
    Map<Id, Set<SObject>> concreteSObjectSetByIdMap = (Map<Id, Set<SObject>>)Type.forName(mapType).newInstance();
    concreteSObjectSetByIdMap.putAll(sObjectSetBySpecifiedIdMap);

    return concreteSObjectSetByIdMap;
}

I'd prefer to invoke it with:

Map<Id, Set<Contact>> mapContactSetByAccountId = (Map<Id, Set<Contact>>) MapHelper.mapBySpecifiedIdField(contactList, Contact.AccountId);

But first, I'll get a compile error:

Incompatible types since an instance of Map<Id,Set<SObject>> is never an instance of Map<Id,Set<Contact>>

If I change my invocation to:

Map<Id, Set<Object>> mapContactSetByAccountId = MapHelper.mapBySpecifiedIdField(contactList, Contact.AccountId);

But this results in a runtime error:

System.TypeException: Invalid conversion from runtime type Map<Id,Set<Contact>> to Map<Id,Set<SObject>>

Is there any way I can make this work?

10
  • 3
    Is there any particular reason why you're using a Set<SObject> as the map value? The problem here lies with trying to upcast a set. If you use a List<SObject> or Map<Id, SObject> in its place, your idea should work.
    – Derek F
    Apr 20, 2018 at 12:30
  • I was using Set because there might be more than one SObject and order doesn't matter. You are correct. This will work with Map<Id, List<SObject>> instead of Map<Id, Set<SObject>>. Feel free to add an answer so I can mark this as solved and give you credit. :-) Apr 20, 2018 at 12:38
  • 3
    I disagree that this is a duplicate - the issue is how to design the method definition to be able to call a method that does the object association, not how to do the association itself. Even if Adrian is closing as duplicate since he provides a solution to this question in the other question, his answer still does not explain why the code in the OP does not work. Apr 20, 2018 at 14:36
  • It is inaccurate to imply a causal relationship between my answer on that question and my closure of this one. I closed it because the ultimate goal of this code is exactly the same as what was posted there. It just happens to be easier to find and remember duplicates to those questions I answered. I think there is also a good duplicate posted by sfdcfox which deals more specifically with why this approach doesn't work, but I haven't been able to locate it.
    – Adrian Larson
    Apr 20, 2018 at 17:39
  • There are lots of duplicates. This question has been thoroughly explained on SFSE. I just added 3 more, and there may well still be some I missed. There's nothing wrong at face value with asking a question which has already been answered, but linking them is important.
    – Adrian Larson
    Apr 20, 2018 at 17:47

1 Answer 1

4

In this particular case, the issue is with using a Set<SObject> as the value type for your map. I ran into a similar issue (well, one that generated the same error at least) in Is there a specific reason why we can't upcast Sets?

The long and short of it is that things like Set<SObject> test = new Set<Contact>(); don't work because of how Apex is implemented by Salesforce and because of concepts from type theory that I don't fully grasp (covariance, generics, and type erasure).

One workaround in this case is to use a Map<Id, List<SObject>> instead of your Map<Id, Set<SObject>>. Using a List<SObject> here means you lose the property of sets where duplicates are guaranteed not to exist (which doesn't work very well with SObjects in the first place, so I don't think it's that big of a loss).

If you do need to guard against duplicate SObjects, then a Map<Id, Map<Id, SObject>> would be more appropriate (the outer map key would be the Id field that you specify, whereas the inner map key would be the Id field of the SObject being stored).

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