I have a ridiculous map of this type: Map<String, Map<Date, Map<Time, Map<Campaign, List<ID>>>>>
. When I render the Visualforce page I get This map cannot be used in an iteration because the keys cannot be sorted.
Which key sets are causing this error to be thrown, why, and how can I work around it? Based on this, I'll probably need a wrapper class somewhere.
-
The campaign key most likely. As for how to fix it, no-one can say for sure as we do not know your use case nor do we know your code....– EricCommented Nov 11, 2015 at 23:21
1 Answer
It appears to me that the problem you're having is because the innermost map's key is an object: Campaign
as opposed to being a primitive or an Id. You won't be able to iterate on a campaign, but you could on the Campaign's Id.
Edit To answer you question on why Campaign wouldn't work as a key, it has to do with how equality of sObjects is determined. Imagine the following situation. You query Campaign records as follows:
list<Campaign>cmpgn = [SELECT Id, Name, Description, Status, Type, OwnerId WHERE OwnerId =: U0.Id];
Now, let's say you run the sam query, but add the following additional fields to it: StartDate
, EndDate
, and Budget
. Obviously, it will return the same results in terms of record Ids. However, the two lists will not be equivalent if you compared them because of the additional fields; important criteria to have for map keys. The only way to compare them for "uniqueness" is using Hash methods. See these two references System hashCode() and Using Custom Types in Map Keys and Sets.
To further illustrate, assume you took the results of the query cmpgn
and reassigned the OwnerId to another user. Clearly once you did that, each of the sObject records would no longer be equal to the previous records returned from the query even though they contained the same Id. Again, using hash methods is the only way to detect that. This is one of the reasons that MVP and author Dan Appleman in his book Advanced Apex Programming, states "Avoid Using Objects as Keys".
So you're wondering what do you do? Dan recommends that you use the object's position in the list when you put it into the map as in cmpgn[x]
and also create another list of campaigns that you modify a field on and assert them as being equal like below:
// create a map keyed on campaign Id against it's location in the list
map<Id, integer>CmpgnMap = new map<Id, integer>();
for(integer i=0;i<cmpgn.size();i++){
CmpgnMap.put(cmpgn[i].Id, i);
}
// Let's use the Description field for our purposes since it's a string and
// we don't expect it to change during our operations
// create another list of campaigns to reference the list we queried
list<Campaign>samecmpgn = new list<Campaign>();
for(integer i=0;i<cmpgn.size();i++){
samecmpgn[i].Description = cmpgn[i].Description + 'Some String' + String.ValueOf(i);
// in this manner we're not creating a clone
system.assertEquals(cmpgn[i].Description,samecmpgn[i].Description);
system.assertEquals(CmpgnMap.get(cmpgn[i].Id,i);
// This is different than you'd expect
// Dan is saying "i = i" here.
CmpgnMap.put(samecmpgn[i].Id,i)
// Since "i=i", we can get and set sobjects from the either list into the map without any problems
}
sytem.assertEquals(CmpgnMap.size(),cmpgn.size());
You should now be able to pull data in and out of your map between the two different lists. You might describe this is as a form of casting.
The main point is that Dan uses the list position along with the Id and maps it against an integer when he does this. I don't know what Id you're mapping Campaign against or what it is from Campaign that you later want to retrieve to truly suggest how best to handle this. If there's a field you know of that won't be changing, you could use that instead of the Campaign Id.
The real key is that you want to create a 2nd list that you can assert like the above to help you transfer values in and out of that portion of the map. You'll also want to create an integer correlation map which will add another layer. There are other methods to handle this, but this one that I know of that's well documented.
-
Could you elaborate on why the Campaign wouldn't work as a key and include that in your answer? I was previously under the belief that SObjects sorted by ID, but that's apparently not the case.– ricksmtCommented Nov 12, 2015 at 0:20
-
Salesforce hashes all of the fields of the sObject when the object is used as a key. See here: salesforce.stackexchange.com/questions/12450/… And for a use-case which takes advantage of that: salesforce.stackexchange.com/questions/71423/… So, you'd be better using Id.– AidanCommented Nov 12, 2015 at 8:15
-
-
Makes sense unless you converted them to strings first and controlled the format of the conversion order to begin with year, month and day followed by the time elements. The separator elements may or may not be an issue, depending on which ones you used with periods probably being the "best" for sorting. I'm also assuming numeric values here. What's most important though is that they be unique if they're going to be used as keys. You'd then need to parse them back to
Date
orDate-Time
values when finished. A utility class would be helpful if you're going to do that. Commented Nov 17, 2015 at 17:37