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?

Adrian Larson
  • 149,971
  • 38
  • 239
  • 420
Brian Kessler
  • 4,558
  • 2
  • 25
  • 66
  • 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 '18 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> instead of Map<Id, Set>. Feel free to add an answer so I can mark this as solved and give you credit. :-) – Brian Kessler Apr 20 '18 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. – IllusiveBrian Apr 20 '18 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 '18 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 '18 at 17:47
  • I think we have very different definitions of "duplicate". While the topics are certainly related and understanding "why" is very suggestive towards "how", the two are not the same. At the time I posted my question, I did not realise Sets were the problem, nor had I ever heard the term "upcast" before. It was just "luck" that I tried doing this with Sets instead of Lists or Maps as @Derek most usefully suggested. – Brian Kessler Apr 20 '18 at 21:25
  • The fact you weren't familiar with the exact terminology or mechanics behind it doesn't have anything to do with if a question is a duplicate. We get dozens of questions a week about, for example, how to write a trigger to roll up data. Do you disagree the topic is thoroughly explained between the answers I linked? – Adrian Larson Apr 21 '18 at 15:17
  • That's the wrong question. I wasn't asking "Why doesn't this work", I was asking "How can I get this to work". If someone just posted new code and didn't explain why it worked nor why my code didn't, I might have been satisfied. Granted, I am glad to have learned the explanation, but English offers different words for "How" and "Why" because they communicate different wants and needs. Moreover, if a third party just read the linked information without my original code, he might not have had a complete solution (e.g. consider the mapType). – Brian Kessler Apr 22 '18 at 11:24
  • The "how" is answered in the first thread I linked as a duplicate. Use List rather than Set. I stand firmly behind that vote; this question is asking how to accomplish exactly the same end goal. I added links to "why" answers because you disagreed with the closure of this question. It seems hard to believe that either "how" or "why" is unanswered after reading all four questions I have linked. Regardless, this conversation is off topic here. If you would like to further explore whether or not this question is a duplicate, that conversation belongs on Meta. – Adrian Larson Apr 26 '18 at 04:47
  • I do not feel like moving this conversation to a closet. First, while the answers to the other questions may have revealed the problem with my approach, they do not answer the question. There would be a significant gap regarding the Map type if a reader only had the other articles to go by. Second, even if the answers were sufficient, a duplicate answer is not a duplicate question. If I ask "Are cats mammals?" and if I ask "Are dogs mammals?", the answers are exactly the same, but these are still different questions no matter how elaborate the answer. – Brian Kessler Apr 26 '18 at 18:19

1 Answers1

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).

Derek F
  • 61,401
  • 15
  • 50
  • 97