Streamlining Data Transformation Into Maps Using The CollectionUtils Class In Salesforce

Opening Statement:

Welcome to our latest post on “Streamlining Data Transformation with the CollectionUtils Class in Salesforce”.

Tired of repetitive for loops to convert sObject lists into maps? Seeking a generic, reusable, and type-safe solution for your data transformation requirements? Well, you’re in luck! The CollectionUtils library class is your answer.

This library class houses a suite of generic and type-safe collection methods, revolutionizing how you interact with your Salesforce data.

In this blog post, we’ll explore how the CollectionUtils library class can efficiently create maps, eliminating the need for endless for loops, saving developers valuable time.

Explanation of the Code:

The library offers three primary methods: idMapFromCollectionByKey, stringMapFromCollectionByKey, and idMapFromCollectionByKeyForList. These methods simplify data transformation, allowing more focus on crucial tasks.

  1. idMapFromCollectionByKey: This method generates a Map<Id, Some_SObject> from a list, eliminating the need for multiple for loops to convert an sObject list into a map where the key differs from the Id field. To ensure type safety, this method accepts a generic list of sObjects and determines the concrete sObject type from the incoming list’s initial object. It casts the concretely typed map to a Map<Id, sObject> and uses generic sObject methods to construct the map.
  2. stringMapFromCollectionByKey: Similar to idMapFromCollectionByKey, this method returns a map with string keys. The key parameter must be convertible to a string, and ensuring the key’s value uniqueness is the developer’s responsibility.
  3. idMapFromCollectionByKeyForList: Accepts a List of sObjects and generates a Map<Id, List<sObject>>. This method helps avoid excessive for loops by simplifying tasks like transforming a list of Contacts into a Map of AccountIds to a List of Contacts.

Example Code:

Presented below is an example of the CollectionUtils class –

/**
* @description Library of generic, type safe collection methods.
* @group Collection Recipes
*/
public with sharing class CollectionUtils {
    /**
    * @description This is a generic, reusable but still typesafe method for
    * generating a Map<Id, Some_SObject> from a list. This code is intended to
    * prevent developers from writing countless for loops just to transform a
    * list of sobjects into a map where the key is something other than the
    * object's Id field.
    *
    * In order to maintain type safety, this accepts a generic list of
    * sObjects. It then determines the concrete sObject type of the incoming
    * lists' first object. This is used to create a new map of type
    * Map<Id, firstItemsType> However, to maintain the generic nature of this,
    * that concretely typed map is cast to a Map<id, sObject>. We then use
    * generic sObject methods of .get() and .set() to construct the map.
    *
    * This works for two reasons:
    * * Because we can always go from a concrete type, say `Account` to the
    *   generic sObject type
    * * When you construct a concrete object but cast it to an sObject, even in
    *   a map context, the concrete sObject type is not lost.
    *
    * @param key String representation of the field name to use as the Key.
    * This must be of type Id for this method.
    * @param incomingList Any list of objects that can be cast to a list of
    * sObjects
    * @return            `Map<Id, sObject>`
    * @example Id key from same object
    * Contact[] contacts = [SELECT AccountId, firstName, lastName FROM Contact LIMIT 50];
    * Map<Id, Contact> contactsByAccountId = (Map<Id, Contact>) CollectionUtils.idMapFromCollectionByKey('accountId', contacts);
    * * @example Id key from from diffrent related object only single level
    * Contact[] contacts = [SELECT AccountId,Account.OwnerId, firstName, lastName FROM Contact LIMIT 50];
    * Map<Id, Contact> contactsByAccountId = (Map<Id, Contact>) CollectionUtils.idMapFromCollectionByKey('Account.Owner.ManagerId', contacts);
    */
    public static Map<Id, SObject> idMapFromCollectionByKey(
        String key,
        List<SObject> incomingList
    ) {
        // Get the object type of the incoming list of SObjects
        String objType = getSobjectTypeFromList(incomingList);
        // Create a dynamic map type using the object type
        Type dynamicMapType = Type.forName('Map<Id,' + objType + '>');
        // Create an instance of the map type
        Map<Id, SObject> returnValues = (Map<Id, SObject>) dynamicMapType.newInstance();
        // Flag to indicate if the key contains a parent object
        Boolean isParentObjectInKey;
        // Split the key into parts based on the "." separator
        List<String> fieldParts = key.split('\\.');
        // Check if the key contains a parent object
        isParentObjectInKey = (fieldParts.size() > 1) ? true : false;
        // If the key contains a parent object, get the parent object name
        String parentObjectName = fieldParts[0];
        // If the key contains a nested field, get the field name
        String nestedFieldName = fieldParts.size() > 1 ? fieldParts[1] : null;
        // Loop through the incoming list of SObjects
        for (SObject current : incomingList) {
            // If the key doesn't contain a parent object
            if(!isParentObjectInKey){
                // Check if the key field value is not null
                if (current.get(key) != null) {
                    // If the key field value is not null, add the key-value pair to the returnValues map
                    returnValues.put((Id) current.get(key), current);
                }
            } else {
                // Get the key value using the parent object and nested field
                String keyValue = (String) current.getSobject(parentObjectName)?.get(nestedFieldName);
                // Check if the key value is not blank
                if(!String.isBlank(keyValue)){
                    // If the key is not blank, add the key-value pair to the returnValues map
                    returnValues.put((Id) keyValue, current); 
                }
            }
        }
        // Return the final map of key-value pairs
        return returnValues;
    }
 
     
    /**
    * @description        Method functions as the above methods do, but returns
    * a map whose keys are strings. The key parameter here must be something
    * castable to string. Note, you are responsible for ensuring the uniqueness
    * of the key's value when using this.
    * @param key          String field name of a field who's value is castable to String.
    * @param incomingList List of incoming sObjects to build the map from
    * @return            `Map<String, sObject>`
    * @example key from same object
    * Contact[] contacts = [SELECT id,firstName, lastName,email FROM Contact LIMIT 50];
    * Map<String, Contact> contactsByAccountId = (Map<String, Contact>) CollectionUtils.stringMapFromCollectionByKey('email', contacts);
    * @example key from diffrent related object only single level
    * Contact[] contacts = [SELECT Account.Name, firstName, lastName FROM Contact LIMIT 50];
    * Map<String, Contact> contactsByAccountId = (Map<String, Contact>) CollectionUtils.stringMapFromCollectionByKey('Account.Name', contacts);
    */
    public static Map<String, SObject> stringMapFromCollectionByKey(
        String key,
        List<SObject> incomingList
    ) {
        // Get the object type of the incoming list of SObjects
        String objType = getSobjectTypeFromList(incomingList);
        // Create a dynamic map type using the object type
        Type dynamicMapType = Type.forName('Map<String,' + objType + '>');
        // Create an instance of the map type
        Map<String, SObject> returnValues = (Map<String, SObject>) dynamicMapType.newInstance();
        // Flag to indicate if the key contains a parent object
        Boolean isParentObjectInKey;
        // Split the key into parts based on the "." separator
        List<String> fieldParts = key.split('\\.');
        // Check if the key contains a parent object
        isParentObjectInKey = (fieldParts.size() > 1) ? true : false;
        // If the key contains a parent object, get the parent object name
        String parentObjectName = fieldParts[0];
        // If the key contains a nested field, get the field name
        String nestedFieldName = fieldParts.size() > 1 ? fieldParts[1] : null;
        // Loop through the incoming list of SObjects
        for (SObject current : incomingList) {
            // If the key doesn't contain a parent object
            if(!isParentObjectInKey){
                // Check if the key field value is not null
                if (current.get(key) != null) {
                    // If the key field value is not null, add the key-value pair to the returnValues map
                    returnValues.put((String) current.get(key), current);
                }
                // If the key contains a parent object
            } else {
                // Get the key value using the parent object and nested field
                String keyName = (String) current.getSobject(parentObjectName)?.get(nestedFieldName);
                // Check if the key name is not blank
                if(!String.isBlank(keyName)){
                    // If the key name is not blank, add the key-value pair to the returnValues map
                    returnValues.put(keyName, current); 
                }
            }
        }
        // Return the final map of key-value pairs
        return returnValues;
    }
 
     
    /**
    * @description        This method accepts an incoming List of sObjects
    * and generates a Map<id,List<sObject>>. Useful for not littering your
    * codebase full of for loops to, for instance, take a list of Contacts
    * and get a Map of AccountIds to a List<Contacts>.
    * @param key          String name of an field that is of the ID type.
    * @param incomingList List of sObjects to build the map from.
    * @return            `Map<Id, List<sObject>>`
    * @example
    * Contact[] contacts = [SELECT AccountId, firstName, lastName FROM Contact LIMIT 50];
    * Map<Id, List<Contact>> contactsByAccountId = (Map<Id, List<Contact>>) CollectionUtils.idMapFromCollectionByKey('accountId', contacts);
    */
    public static Map<Id, List<SObject>> mapFromCollectionWithCollectionValues(
        String key,
        List<SObject> incomingList
    ) {
        // Get the object type of the incoming list of SObjects
        String objType = getSobjectTypeFromList(incomingList);
        // Create a dynamic list type using the object type
        Type listObjType = Type.forName('List<' + objType + '>');
        // Create a dynamic map type using the object type and list type
        Type dynamicMapType = Type.forName('Map<Id, List<' + objType + '>>');
        // Create an instance of the map type
        Map<Id, List<SObject>> returnValues = (Map<Id, List<SObject>>) dynamicMapType.newInstance();
        // Loop through the incoming list of SObjects
        for (SObject current : incomingList) {
            // Check if the key field value is not null
            if (current.get(key) != null) {
                // Check if the map already contains the key
                if (returnValues.keySet().contains((Id) current.get(key))) {
                    // If the map contains the key, retrieve the existing list and add the current SObject to the list
                    List<SObject> existingList = (List<SObject>) returnValues.get((Id) current.get(key));
                    existingList.add(current);
                    returnValues.put((Id) current.get(key), existingList);
                } else {
                    // If the map does not contain the key, create a new list and add the current SObject to the list
                    List<SObject> newList = (List<SObject>) listObjType.newInstance();
                    newList.add(current);
                    returnValues.put((Id) current.get(key), newList);
                }
            }
        }
        // Return the final map of key-value pairs, where the value is a list of SObjects
        return returnValues;
    }
 
    /**
     * Get the SObject type of the objects in the incoming list
     *
     * @param incomingList list of SObjects
     * @return the name of the SObject type
     */
    private static String getSobjectTypeFromList(List<SObject> incomingList) {
        // Return the SObject type of the first item in the list if the list is not empty, 
        // otherwise return "sObject" as the default value
        return (!incomingList.isEmpty())
            ? String.valueOf(incomingList[0]?.getSObjectType())
            : 'sObject';
    }
}