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.
idMapFromCollectionByKey
: This method generates aMap<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 aMap<Id, sObject>
and uses generic sObject methods to construct the map.stringMapFromCollectionByKey
: Similar toidMapFromCollectionByKey
, 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.idMapFromCollectionByKeyForList
: Accepts a List of sObjects and generates aMap<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'; } }