Search
Close this search box.

Improving Apex Fluidity Using Streams

Red Argyle Logo without name
Red Argyle logo

As Salesforce Developers, we all know the tried and true first rule of Apex development; No SOQL/DML in a loop!

On a related note, you could argue that the second rule should be to always write your database operations in bulk.

Both of these are very important for adhering to the governor limits imposed on us as tenants in the multi-tenant ecosystem of Salesforce, which drives us to write Apex code in a specific way. This means we often find ourselves repeating the same patterns over and over. 

One of the most common ones we see is needing to group records by a shared field value, such as Contacts grouped by their related Account. While this is something that can be done directly using SOQL’s GROUP BY clause, we don’t want to have to requery every time we need to group by a different field. This is often the case in Triggers, where we are given the records without needing to query. Here’s an example of how we might handle this programmatically:

List<Contact> contacts = [SELECT Id, AccountId FROM Contact];
Map<Id,List<Contact>> contactsByAccountId =  new Map<Id,List<Contact>>();
for (Contact c : contacts) {
  		if (!contactsByAccountId.containsKey(c.AccountId)) {
       		contactsByAccountId.put(c.AccountId, new List<Contact>());
   		}
   		contactsByAccountId.get(c.AccountId).add(c);
}

This results in a Map data type where an account is mapped to a list of the related Contacts. This amount of code is overbearing for what should be a simple operation. Especially if we need to do this multiple times in one operation, we can simplify by using the following function:

 // Utility function
   public static Map<Id, List<SObject>> mapRecordsByIdField(List<SObject> records, 
   String field) {
       Map<Id, List<SObject>> recordsMap = new Map<Id,List<SObject>>();
       for (SObject record : records) {
           Id key = (Id) record.get(field);
           if (!recordsMap.containsKey(key)) {
               recordsMap.put(key, new List<SObject>());
           }
           recordsMap.get(key).add(record);
       }
       return recordsMap;
   }
 
   // Example
   List<Contact> contacts = [SELECT Id, AccountId FROM Contact];
   Map<Id,List<Contact>> contactsByAccountId = mapRecordsByIdField(contacts, 
   'AccountId');

Nice! We’ve condensed this operation into a function that we can reuse wherever we need it. Luckily, we don’t have to cast the output of our method due to the semi-generic properties of SObjects (or more specifically, List<SObject>).

Now let’s take a look at a more simple example; getting a Set of values from a List of SObjects. Here is the example utility function:

// Utility function
   public static Set<Id> getUniqueIdFieldValues(List<SObject> records, String field) {
       Set<Id> results = new Set<Id>();
       for (SObject record : records) {
           results.add((Id) record.get(field));
       }
       return results;
   }
 
   // Example
   List<Contact> contacts = [SELECT Id, AccountId FROM Contact];
   Set<Id> accountIds = getUniqueIdFieldValues(contacts, 'AccountId');

Lists and Sets are almost the same in Apex, but we’re using a Set here because in most cases:

  • You do not want duplicate values
  • Indexes or the ordering of items do not matter
  • You get a more efficient contains() method, since Sets Hash the items 

While this utility method is definitely useful, it may not be as intuitive or customizable as we would like it to be. To show this, let’s take a look at a Javascript snippet:

const accountIds = contacts.map((c) => c.AccountId);

Now that’s what we call clean code! Built in a more concise fashion, this snippet is still functionally equivalent,minus the unique-ness property of Sets, to our previous Apex snippet.. It takes an array of objects (Contacts) and transforms (maps) that array into an array of Ids, taken from the AccountId attribute of each object. In Javascript, the map(), filter(), and reduce() methods of arrays allow functional operations (lambda expressions) to be applied to each member of a collection. These methods are also available in Java, as part of the Stream package.

But what does this mean for Apex?

Since these are not native language features of Apex, we would have to build them out ourselves, similar to Java. However, the main problem preventing us from nicely implementing map, filter, and reduce is the lack of anonymous (lambda) functions in Apex. It is understandable why lambdas are not currently supported, since there is a focus on statically-typed, statically-defined code that is easy to test and scale. Thankfully, this is a feature on Salesforce’s roadmap, which will open up a lot of new opportunities once it’s here. In the meantime, we can do our best to implement Java Streams in Apex.

At its core, Streams are just collections; lists that can be iterated on. We can then apply operations to each entry (map), filter records based on criteria (filter), or produce a singular output based on all entries (reduce). Without having anonymous functions, we cannot write these operations in-line, so we will have to create them ahead of time. These will be defined as implementations of an interface with one method, which takes an input and produces an output. The Stream class will then have mapTo(), filterTo(), and reduceTo() functions that apply a given operation to the inner collection.

Let’s take a look at that first utility function we made:

 // Example
   List<Contact> contacts = [SELECT Id, AccountId FROM Contact];
   Map<Id,List<Contact>> contactsByAccountId = mapRecordsByIdField(contacts, 
   'AccountId');

Now let’s look at how we can do this with our Stream class:

  // Example
   List<Contact> contacts = [SELECT Id, AccountId FROM Contact];
   Map<Id,List<Contact>> contactsByAccountId =                                                                                  
   Stream.of(contacts).toSObjectListIdMap(Contact.AccountId, Contact.class);

The Stream.of() method is a static method that creates a Stream from a given List of objects. Then, the toSObjectListIdMap() method performs the mapping operation to create the result. The method is written almost exactly the same as our original utility method, but is built as a reduce operation of the Stream. One nice thing we can implement here is a fluent interface. All three core methods of the Stream return a modified version of the original Stream, which not only makes our class immutable, but also allows us to do fancy method chaining like this:

 // Gets unique AccountIds where Contact has FirstName
List<Contact> cons = [SELECT Id, AccountId FROM Contact];   
Set<Id> accountIds = Stream.of(cons)
       .filterToField(new Stream.NotNull(), Contact.FirstName)
       .mapTo(Contact.AccountId)
       .uniqueIds();

The filterToField() method used here is a special case. Normally, filterTo() applies operations such as Stream.NotNull() to the entries themselves, but we often want to filter on fields of those entries, hence filterToField(). 

Streams are one way we can streamline our development by creating reusable and readable code. They end up being extremely useful since we are always working on collections of records in Apex. Until we get native support for anonymous functions, we can continue to write helper functions around Streams to simplify syntax and increase readability.

Red Argyle logo
Red Argyle logo

Related Blog Posts