Working with Maps in Apex Class
Instantiating a Map
Here is the standard way of instantiating a map:
Map<Id, Account> accountsById = new Map<Id, Account>();
put()
method. However, if you happen to have a list of sObjects you can just pass that list in the constructor like so:Map<Id, Account> accountsById = new Map<Id, Account>(listOfAccounts);
trigger.new
property and add them to a set. However, a better option would be to do this:Map<Id, Account> accountsById = new Map<Id, Account>(trigger.new);
Set<Id> accountIds = accountsById.keySet();
List<Contact> contacts = [SELECT Id, FirstName, LastName FROM Contact WHERE AccountId IN :accountIds];
As you can see, no loops were needed to get a set of account ids. We simply passed the list of accounts to the new map instance and then used the maps keySet()
method to get a list of ids for us.
As a bonus, Apex Trigger methods provide a map of sObjects for us. So we can simplify this example even further.
Set<Id> accountIds = trigger.newMap.keySet();
List<Contact> contacts = [SELECT Id, FirstName, LastName FROM Contact WHERE AccountId IN :accountIds];
Tip: if you have a list of sObjects you don't have to loop through and get a set of ids. You could just pass that list of sObjects to your query like this (assuming you have a list of account records in a variable called
List<Account> accounts
.
[SELECT Id FROM Contact WHERE AccountId IN :accounts]
Apex is smart enough to know how to get the account ids from the list and to apply them as part of the
WHERE
clause.
But what if you don't have a list of sObjects and you need to do a query to get them. Well you can just pass that query directly to the map like so:
Map<Id, Account> accountsById = new Map<Id, Account>([SELECT Id, Name FROM Account LIMIT 10]);
On the other hand, maybe you just have a set of static values or variables that you want to add to a map. Well, you can do that during instantiation as well:
Map<string, string> stringMap = new Map<string, string>{ 'Key1' => 'Value1', 'Key2' => val2, key3 => val3};
Avoiding Governor Limits
To explore this topic let's say that you need to write a trigger that sets a value on an opportunity based on some information found at the line item level.
If query limits were not an issue you might do something like the following:
for (Opportunity opp : trigger.new) {
List<OpportunityLineItem> lineItems = queryOppLineItems(opp.Id);
for (OpportunityLineItem lineItem : lineItems) {
/*Do Something*/
}
}
In this example, the code will compile and will even run without error when not executed as part of a batch transaction. But anything that executes your trigger in the context of a batch such as importing data through the API or mass record edit tools will cause an error.
Also, your code is not run in isolation; execution will take place as part of a transaction that will include other blocks of code that are configured to execute under the same context. Thus, any queries, DML operations, etc. will count towards the same set of limits. That is why executing a query inside of a loop is considered a bad practice. Not only do you run the risk of hitting a limit within your block of code but when combined with the usage incurred by related code in the system you are at higher risk of reaching a limit.
Given our new understanding of how Apex limits the number of query and DML operations your code can make, you can start to see how Maps become a useful tool to help you avoid these types of limitations. For example, rather than retrieve opportunity line items within a loop, first query all line items related to the current list of opportunities. Then, organize them into a map where the key is the opportunity id and value is a list of opportunity line items associated with that opportunity id.
Our previous example now becomes this:
Set<Id> oppIds = trigger.newMap.keySet();
Map<Id, List<OpportunityLineItem>> lineItemsByOppId = Map<Id, List<OpportunityLineItem>>();
List<OpportunityLineItem> lineItems = queryOppLineItems(oppIds);
for (OpportunityLineItem lineItem : lineItems) {
if (lineItemsByOppId.containsKey(lineItem.OpportunityId)) {
lineItemsByOppId.get(lineItem.OpportunityId).add(lineItem);
}
else {
lineItemsByOppId.put(lineItem.OpportunityId, new List<OpportunityLineItem>{ lineItem });
}
}
for (Opportunity opp : trigger.new) {
if (lineItemsByOppId.containsKey(opp.Id)) {
List<OpportunityLineItem> oppLineItems = lineItemsByOppId.get(opp.Id);
for (OpportunityLineItem lineItem : oppLineItems) {
/*Do Something*/
}
}
}
Organizing and Caching Data
For example, maybe your organization makes heavy use of record types, and you need to access these types to execute specific sets of logic. You could just query the record types as needed and then loop through and find the value you need but what if you need to do this for more than one method, class, or trigger.
One way of organizing your record types for use would be to do something like this:
Map<string, Map<string, RecordType>> recordTypesByTypeAndName = new Map<string, Map<string, RecordType>>();
List<RecordType> recordTypes = [SELECT Id, Name, DeveloperName, SObjectType, IsActive FROM RecordType];
for (RecordType rt : recordTypes) {
if (recordTypesByTypeAndName.containsKey(rt.SObjectType)) {
Map<string, RecordType> recordTypeByName = recordTypesByTypeAndName.get(rt.SObjectType);
if (recordTypeByName.containsKey(rt.DeveloperName) == false) {
recordTypeByName.put(rt.DeveloperName, rt);
}
}
else {
recordTypesByTypeAndName.put(rt.SObjectType, new Map<string, RecordType>{ rt.DeveloperName => rt });
}
}
public Id getRecordTypeId(string objectType, string name) {
Id recordTypeId = null;
if (recordTypesByTypeAndName.containsKey(objectType)) {
Map<string, RecordType> recordTypesByName = recordTypesByTypeAndName.get(objectType);
if (recordTypesByName.containsKey(name)) {
RecordType rt = recordTypesByName.get(name);
recordTypeId = rt.Id;
}
}
return recordTypeId;
}
I hope this article gave you some ideas of how you can make Maps work for you and improve the readability and performance of your applications. As always, if you have any questions or comments feel free to leave a comment below.