I’ve had a pleasure to watch many Salesforce technical architects trying to find a perfect way, how to work with Record Types in Apex. One created class Global_Constants to store all Record Type Ids and DeveloperNames in static properties, the other queried RecordType table and put the result in the map. I was never impressed so I‘ve come up with Record Type Provider, that allows you to access RecordTypeId like this: RecordTypeProvider.[ObjectName].[RecordTypeName].Id. It’s not perfect, but it definitely has its pros.
What doesn’t work
First, I would like to stop by the usual solutions and explain, why they do not work (well).
Querying the RecordType table is extremely fast – you get all the Record Type descriptions in few miliseconds, but on the other hand you lose what every Salesforce developer must treasure the most – SOQL query. You may think, that you have only limited amount of classes and this one tiny query will never mean anything, but over time logic in your org may get very complicated and if you keep wasting your queries like that, you will find yourself refactoring the whole piece soon to avoid reaching governor limits.
Global Constants
Having all the common constants (public static final) stored in one place is definitely a valid idea – if you need to change a value you are comparing against in multiple classes, you do it in one place only. On the other hand, when the developer reaches for any static property in the class, every static property in that class is evaluated at that moment. It is not very painful, when constants are assigned with hard-coded text, but some evaluations (especially when there is many of them) might get pretty messy. But I admit, that since Salesforce is using lazy loading in its „getDescribe mechanisms“, getting Record Type info doesn’t hurt anymore.
Provider
Idea behind my solution is to provide developers nice, fast, consistent and comprehensible way to access Record Types in Apex, that would embrace Intellisense. I wondered if I would be able to work with Record Types like this: RecordTypeProvider.Account.SMB.Id or like this RecordTypeProvider.Account.Enterprise.DeveloperName. And the answer is I can. Let’s check the code and discuss the pros and cons later.
To be able to use following code, create following Record Types:
DeveloperName: RecordType, Label: Small and Medium-Sized Businesses
DeveloperName: Enterprise, Label: Enterprise
Implementation
public with sharing class RecordTypeProvider {
/**
* Definition of SObjects supported by this class
* Objects used in this part are not SObjects, but inner classes from withing this class
* Those objects only refer to SObjects
* You cannot use __c for referring to custom SObjects
*
* We should use capital ACCOUNT as a property name to follow best practice,
* But to me in this special case, it makes much more sense to "mimic" SObject name
*
* Nested classes do not support static properties, therefore we need to instantiate them
*/
public static final Account Account = new Account();
/**
* Private properties
*/
// Register of all Record Types per SObjectType
private static Map<String, Map<String, Schema.RecordTypeInfo>> recordTypesInfo = new Map<String, Map<String, Schema.RecordTypeInfo>>();
/**
* @description Private constructor forces using class only from static context
* @param
* @return RecordTypeProvider
*/
@TestVisible
private RecordTypeProvider() {
}
/**
* @description Retrieves, stores and provides Record Type Info
* @param SObjectType Specifies for which SObject we need Record Type Info
* @param String Requested Record Type Developer Name
* @return Schema.RecordTypeInfo
*/
private static Schema.RecordTypeInfo getRecordTypeInfo(SObjectType objectType, String recordTypeDeveloperName) {
// Check register for RecordTypeInfo for specific object
String objectTypeName = String.valueOf(objectType);
Map recordTypesInfoForSObject = recordTypesInfo.get(objectTypeName);
if (recordTypesInfoForSObject == null) {
// Retrieve RecordTypeInfo for specific object
recordTypesInfoForSObject = objectType.getDescribe(SObjectDescribeOptions.DEFERRED).getRecordTypeInfosByDeveloperName();
recordTypesInfo.put(objectTypeName, recordTypesInfoForSObject);
}
// Get Schema.RecordTypeInfo per Record Type Developer Name
Schema.RecordTypeInfo recordTypeInfo = recordTypesInfoForSObject.get(recordTypeDeveloperName);
if (recordTypeInfo == null) {
// throw exception, when Record Type with provided Developer Name doesn't exist for provided SObject
throw new RecordTypeProvider.RecordTypeProviderException(
'Record Type with Developer Name \'' + recordTypeDeveloperName + '\' doesn\'t exist for \'' + objectTypeName + '\'.'
);
}
return recordTypeInfo;
}
/**
* @description Contains RecordType information
*/
public with sharing class RecordTypeInfo {
// SObjectType of Record Type
private SObjectType objectType;
// Id of Record Type
public Id Id {
get {
if (Id == null) {
populateByDeveloperName();
}
return Id;
}
private set;
}
// Developer Name of Record Type
public String DeveloperName {
get;
private set;
}
// Name (label) of Record Type
public String Name {
get {
if (Name == null) {
populateByDeveloperName();
}
return Name;
}
private set;
}
/**
* @description Constructor setting private properties
* @param SObjectType Specifies SObject of Record Type
* @param String Specifies Developer Name of Record Type
* @return RecordTypeInfo
*/
public RecordTypeInfo(SObjectType objectType, String developerName) {
if (objectType == null || developerName == null) {
// Both objectType and developerName are mandatory
throw new RecordTypeProviderException(
'You have to specify both SObjectType and DeveloperName.'
);
}
this.DeveloperName = developerName;
this.objectType = objectType;
}
/**
* @description Populates Id and Name (Label) for Record Type
* @param
* @return
*/
private void populateByDeveloperName() {
Schema.RecordTypeInfo recordTypeInfo = RecordTypeProvider.getRecordTypeInfo(objectType,DeveloperName);
Id = recordTypeInfo.getRecordTypeId();
Name = recordTypeInfo.getName();
}
}
/**
* @description This class serves as base class for properties representing SObjectTypes
*/
public abstract with sharing class ObjectType {
protected SOBjectType type;
/**
* @description Inits record type info
* @param String RecordType.DeveloperName
* @return RecordTypeInfo
*/
@TestVisible
private RecordTypeInfo newRecordType(String recordTypeDeveloperName) {
return new RecordTypeInfo(type, recordTypeDeveloperName);
}
}
/**
* @description Representation of Account
*/
public with sharing class Account extends ObjectType {
public RecordTypeInfo SMB {get; private set;}
public RecordTypeInfo Enterprise {get; private set;}
/**
* @description Inits object type
*/
public Account() {
type = Schema.Account.SObjectType;
SMB = newRecordType('SMB');
Enterprise = newRecordType('Enterprise');
}
}
/**
* @description Exception for RecordTypeProvider
*/
public with sharing class RecordTypeProviderException extends Exception {
}
}
How does it work?
Abstract class ObjectType
If you want to set-up new SObject to be available in this Record Type Provider you need to extend abstract class ObjectType. Your new class will serve as some kind of substitute for the SObject, so I recommend to name it same way as the original.
The abstract class ObjectType contains property type, that references actual SObjectType, and method newRecordType, which you need to call for each Record Type you want to used in the provider.
Nested class RecordTypeInfo
Nested RecordTypeInfo is used to lazyload Record Type information - Name, DeveloperName and Id based on provied SObjectType and Record Type DeveloperName. You need to instantiate it from ancestor of ObjectType (nested classes do not support static methods and properties). The engine of this class is private method populateByDeveloperName.
Method getRecordTypeInfo
This method may look complicated, but it does a very simple thing. You provide it with SObjectType and Record Type DeveloperName and it returns Schema.RecordTypeInfo, which is later used in populateByDeveloperName method, when you ask for the specific Record Type in your application.
Method saves all the information retrieved for each SObjectType to private static property recordTypesInfo to prevent calling getDescribe multiple times. Yes, the result of getDescribe is cached in Salesforce, but still...
The last step is to create static property of your newly created class extending ObjectType abstract. Again, I recommend to name it same way as the SObject itself. This is the "Account" in the RecordTypeProvder.Account.SMB.Id call after all.
Summary
Pros
- Easy to use (readable and consistent)
- Fast
- Embraces Intellisense
Cons
- Requires maintenance (you need to update Provider class, when adding new Object or Record Type)
Is it worth?
In the end it depends on your taste. But I’ve tried that on two enterprise projects with two different teams of 6 developers and on both of those projects we‘ve saved lot of time during development (not only because we stopped Googling „getRecordTypeIds“), code reviews and bug fixing (readability) and reduced code redundancy. However for small and medium-size projects, this will most probably be an overkill.
Looking for an experienced Salesforce Architect?
- Are you considering Salesforce for your company but unsure of where to begin?
- Planning a Salesforce implementation and in need of seasoned expertise?
- Already using Salesforce but not fully satisfied with the outcome?
- Facing roadblocks in your Salesforce implementation and require assistance to progress?
Feel free to review my profile and reach out for tailored support to meet your needs!
Comments
Post a Comment