Retrieving configuration data in a bulkified way
No book on Apex is complete without some discussion around bulkification. Bulkification is a requirement in Salesforce due to the Governor Limits that are imposed on developers because of the multi-tenant nature of the platform. Many developers that are new to the platform see the Governor Limits as a hindrance rather than an assistance. We will cover this in more detail in Chapter 13, Performance and the Salesforce Governor Limits. However, a common mistake that developers make on the platform is to not bulkify their code appropriately—particularly, triggers. It is also common for intermediate to advanced developers to not bulkify their non-trigger code appropriately either. We will discuss bulkification of triggers more explicitly in Chapter 3, Triggers and Managing Trigger Execution, and will cover querying and Data Manipulation Language (DML) within loops later in this chapter. Firstly, however, I want to discuss bulkifying the retrieval of data that is not typically stored in a custom or standard object—configuration data.
Hot and cold data
I want to begin this section with a discussion on hot and cold data within the system, as well as the implications this has on bulkification. For all of the data within our system, let's assume that the data starts off with a temperature of 0 (our scale should not matter, but let's assume we are using °C, where 0 is freezing and 100 is boiling). Every time that our data is written to, its temperature increases by one degree, and if an entire day goes without it being updated, it drops a degree and decreases by one. If we were to run this thought experiment across our data, we would then obtain a scale for each data type we are retrieving, where the data would range from very cold (that is, hardly ever written to) through to extremely hot (edited multiple times a day).
For most objects in Salesforce, the temperature graph of the data would appear in a long-tailed distribution manner—that is, an initial peak of activity as a record is created and then updated, until it reaches a stage in its life cycle where it is no longer viewed and only really included for auditing and reporting purposes. Think of an opportunity record, for example: a lot of initial activity until it is closed, won, or lost, and then it is used mainly for reporting. When working with these records in Apex, we will need to ensure we query for them to get the latest version for accurate and up-to-date data. As we are in most cases not writing Apex to work on "cold" instances of this data, we need to be aware of the fact that these records may change during the scope of a transaction due to an update we are making. Our normal bulkification practices, discussed next, will help us manage this.
What about data that is truly cold—that is, created for all intents and purposes but never updated? Think of a custom metadata record that holds some configuration used in an Apex process. Such information is created and then only updated when the process itself changes. For such data, we actively want to avoid querying multiple times during a transaction yet ensure that it can be made available across the entire transaction as needed. As Salesforce applications have grown and more custom configuration has been added to make the applications more dynamic and easier to update, more organizations have deployed custom metadata, custom settings, and custom configuration objects (although these are now largely superseded by custom metadata and custom settings). How do we as developers manage retrieval of this data in a manner that is bulkified and that allows us to reuse this data across a transaction?
Retrieving and sharing data throughout a transaction
For this use case, a developer can either use the singleton pattern or make appropriate use of static variables to manage the retrieval of this data. We could implement a singleton utility class, as follows:
public class ExampleSingleton { Â Â Â Â private static ExampleSingleton instance; Â Â Â Â private Example__mdt metadata; Â Â Â Â Â Â Â Â public static ExampleSingleton getInstance() { Â Â Â Â Â Â Â Â if(instance == null) { Â Â Â Â Â Â Â Â Â Â Â Â instance = new ExampleSingleton(); Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â return instance; Â Â Â Â } Â Â Â Â Â Â Â Â private ExampleSingleton() { Â Â Â Â } Â Â Â Â Â Â Â Â public Example__mdt getMetadata() { Â Â Â Â Â Â Â Â if(metadata == null) { Â Â Â Â Â Â Â Â Â Â Â Â metadata = [SELECT Id FROM Example__mdt LIMIT 1]; Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â return metadata; Â Â Â Â } Â Â Â Â }
Our private static
member variable—instance
—holds the unique instance of the ExampleSingleton
class for the transaction. The getInstance
method is the only way of either instantiating or retrieving the instance for use, and we have ensured this by making the default constructor private. We can use this instance to retrieve our Example__mdt
record, using the getMetadata
method on our returned instance. The actual instance of the Example__mdt
record is stored as a private member for the class and is state abstracted away from the change.
The benefit of such an approach is that we can both encapsulate our data and its workings and ensure that we are only ever retrieving the information from the database once. This singleton could be used to hold many different types of data as needed so that the entire transaction can scale in its usage of such data in a common way.
Alternatively, we could implement a static class such as the following one:
public class ExampleStatic { Â Â Â Â private static Example__mdt metadata; Â Â Â Â Â Â Â Â public static Example__mdt getExampleMetadata() { Â Â Â Â Â Â Â Â if(metadata == null) { Â Â Â Â Â Â Â Â Â Â Â Â metadata = [SELECT Id FROM Example__mdt LIMIT 1]; Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â return metadata; Â Â Â Â } Â Â Â Â private ExampleStatic() { Â Â Â Â } }
Again, we have ensured that our metadata will only be loaded once across the transaction and have a smaller code footprint to manage. Note that this is not a true static class, as these are not available in Apex. However, there is a close enough analogy to this in the language that we can consider as a static class (it cannot be externally instantiated due to its private constructor).
For cold data such as custom metadata, custom settings, or any custom configuration in objects, the use of a singleton or a static class can greatly improve bulkification. I have seen instances in production where the same set of metadata records was retrieved multiple times during a transaction as the code began to interact by recursively firing triggers through a combination of updates.
Singleton versus static class
It is a fair question to ask right now whether a singleton or a static class instance should be used in utility classes. The answer (as with all good questions) is it depends. While both have similar functionality from our perspective in terms of retrieving data only once, singletons can be passed around as object instances for use across the application. They can also extend other classes and implement interfaces with the full range of Apex's object-oriented (OO) features. While static classes cannot do this, they are more useful in lightweight implementations where the OO features of the language are not required.