5 min read

Working with Apex Code Maps

Working with Apex Code Maps

Maps are a critical data type to get to know when developing on the Force.com platform. You can use them in a lot of creative ways to make your code more readable, efficient, and scalable.

In this article I thought I would explore some of the ways I use maps.

Instantiating a Map

Maps have some magic behind them that can make your code more readable and efficient. In particular, maps can be instantiated in a number of ways.

Here is the standard way of instantiating a map:

Map<Id, Account> accountsById = new Map<Id, Account>();

Once you have instantiated a map, you can add values to the map simply by using the 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);

The nice thing about this little bit of magic is that you can use it to avoid having to loop through a list of sObjects. When you pass a list of sObjects Apex will automatically add entries to the map by the sObject's id. To get an idea of how useful this technique can be, let's say we have a trigger on the account object, and we need to query contacts for those accounts. Ordinarily, you might choose to loop through the list of accounts in the 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

As you know, Salesforce attempts to encourage efficient code by enforcing what it calls "governor limits." Apex monitors the execution of your code and limits you from excessively leveraging certain system features such as queries, inserts, and updates to name a few. See Understanding Execution Governors and 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

Another great use of maps is to organize your data so that you can avoid nested loops.

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 });
    }
}

Using this method you now have a way to store record type hierarchy information to be reused within a class or globally as a static property. Using this technique, you only consume one query statement for information that can be utilized by multiple blocks of code within the same transaction. Then you can make it easy to access this information using a helper method similar to the following example:

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.