Skip to main content
Added materials from Appleman's book on sObjects as map keys
Source Link
crmprogdev
  • 41k
  • 9
  • 58
  • 116

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.

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.

Source Link
crmprogdev
  • 41k
  • 9
  • 58
  • 116

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.