Entities: SObject wrappers
Wrap Salesforce SObjects with automatically generated entity classes
Salesforce provide us with SObject
types in apex that allow us to programmatically deal with database records. When we add a custom object, Salesforce will automatically create an SObject
type for that new custom object, allowing us to instantly work with records as instances of that new class. This is an out of the box object relational model, for which we are very grateful.
However, it is not possible to augment these types with our own logic. If you’ve instinctively wanted to add or modify a method on an SObject type, this blog post is for you.
- Want do deal with Currency fields with a
Money
class instead ofDecimal
? (Why: SFDC decimal values with scales larger than field metadata can still be persisted beyond field scale) - Want to lazy load related objects later than always querying them in a selector?
- Instead of an SObjectDomain class mixing your trigger logic with the domain, you can define validation/other domain logic inside an
Entity
class - keeping your trigger classes clean - Want helper / utility methods? Maybe you’d like a simple
isActive
method that looks at a status picklist field and returns true if it equals “Active” - You’d like to have different instances of sibling classes in the same hierarchy depending on
RecordType
, e.g. aDonationOpportunity
class versus aProductOpportunity
class both extending aBaseOpportunity
class where instantiation is determined at runtime based on the value ofRecordTypeId
- this way you can have validation/domain logic specific for a product opportunity in a clearly defined entity class for that domain - Good lever for performance optimization - we just recently added changed field tracking to reduce DML operation time (only commit fields that are changed by our code) - we could do this easily because every mutation of an SObject’s field goes through the same call stack
On Traction Rec we have written some code generation tooling to automatically generate these classes based on the metadata xml describing objects, which we use throughout our products today.
We’re going to be focusing on what we do with these entities, why we like them, rather than how we generate them - but we will share some of that generation code here too - it’s just in a toolset you’re unlikely to also be using (gradle).
Overall, we’ve seen a LOT of benefits after implementing Entities in our code, and if you are an ISV working on a larger code base, we’d recommend you achieve the same with some tooling too.
Hopefully this blog post gives you a few ideas and a starting point for your own implementation!
Generation
The process for generating entity classes is
- Find the object & field metadata
- For each object, use a templating engine, output an apex class
Pretty simple right?
In our case we used gradle + FreeMarker to generate our classes. To simplify this process we built a gradle/groovy class that takes the relevant options
task generateWrappers(type: WrapperClassGenerationTask) {
apiVersion projectJson.sourceApiVersion
objectsDirectory new File(project.projectDir, "src/main/default/domain/objects")
srcOutputDirectory new File(project.projectDir, "src/main/apex/domain/entity/generated")
testOutputDirectory new File(project.projectDir, "src/main/test/domain/entity/generated")
namespace = projectJson.namespace + "__"
namespaceJS = projectJson.namespace + "__"
excludedSettersByObjectName = [
Foo__c: ["Bar__c"]
]
}
If you want to see the freemarker template and resulting code samples scroll to the end of this post. For now we’re going to talk about some of the cool things entities let you do. For all the coming examples we’ll look at our “Transaction” class in our payment processing package, and the useful additions we’ve been able to make with entities.
Discrimination
Starting with the most useful, a core concept for us is “Discrimination” between different “types” of entities within the same table. Other ORMs, like hibernate, have this concept - where a different class is used to represent an entity based on some condition. This can be really useful, especially as we get deeper into the world of 2GP packaging - you can define a table in a core package and have different “types” of that table, modelled by different entities, for different domains.
That’s exactly what we do with our Transaction table. Depending on the payment processing integration, you could have a different instance of a TransactionEntity
- it could be a PayPalTransactionEntity
or a StripeTransactionEntity
, each with their own domain specific methods - maybe even with their own PayPal specific fields and stripe specific fields, all defined in different packages.
We find this a perfect place for domain specific validation, for instance a payment processing specific Transaction entity class might have special validations;
public override void doConcreteValidate(List<ValidationError> errors) {
if (getTenderTypeEntity().getRequiresBatching() && (isPartiallyOrFullyApproved() || (isPending() && isBankTransfer()))) {
Validators.validateNotNull(
getVPBatchId(),
TP_Transaction__c.TX_VP_BatchId__c.getDescribe().getLabel(),
errors
);
}
}
There’s a few things required to add discrimination to your entities;
EntityFactory
An entity factory is an abstract class that defines the following;
protected abstract BaseEntity wrap(SObject record);
Concrete implementations of this factory can be used to call a concrete constructor of an Entity class, for example, our StripeTransactionEntity
class could have an inner Factory
class that extends this EntityFactory
abstract class;
public class Factory extends EntityFactory {
public Factory() {
super(Transaction__c.SObjectType);
}
public override BaseEntity wrap(SObject record) {
return new StripeTransactionEntity(
(TP_Transaction__c) record
);
}
}
We can see that this inner class provides a mechanism to construct a StripeTransactionEntity
via the wrap(SObject record)
method.
We rely on these factory classes instead of a default constructor and a record setter because it should be defined at construction what the record of an Entity
is;
- The compiler guarantees that no Entity exists without wrapping a record
- Developers don’t need to ensure they call a setter
- Allows us to add more constructor arguments easily in the future
Now you might be asking yourself, given a Transaction__c
record, how does our application know to wrap it with the StripeTransactionEntity
class? That’s where resolution strategies come in.
Strategic Entity Factory
The strategic entity factory produces entity factories via resolution strategies. This provides a good mechanism for determining, at runtime, what class to wrap a given SObject with.
An entity resolution strategy is just an interface that defines the following;
public interface IEntityFactoryResolutionStrategy {
EntityFactory resolve(SObject record);
}
Any class that implements this interface can be used to return an EntityFactory instance for a given SObject record.
Let’s look at an example strategy implementation for a record type;
First let’s define a FieldValueEntityFactoryStrategy class that can resolve to a specific EntityFactory based on a field value;
@NamespaceAccessible
public with sharing class FieldValueEntityFactoryStrategy implements IEntityFactoryResolutionStrategy {
// =========================================================
// ATTRIBUTES
// =========================================================
private final SObjectField targetField;
private final Map<String, EntityFactory> entityFactoryByFieldValue;
// =========================================================
// CONSTRUCTORS
// =========================================================
@NamespaceAccessible
public FieldValueEntityFactoryStrategy(
SObjectField targetField,
Map<String, EntityFactory> entityFactoryByFieldValue
) {
try {
Preconditions.checkNotNull(targetField, 'Provided null targetField, field must not be null');
for (String key : entityFactoryByFieldValue.keySet()) {
Preconditions.checkNotNull(
entityFactoryByFieldValue.get(key),
'Provided null EntityFactory for field value: ' + key
);
}
} catch (Preconditions.PreconditionException ex) {
throw new ConfigurationException('Invalid configuration for FieldValueEntityFactoryStrategy', ex);
}
this.targetField = targetField;
this.entityFactoryByFieldValue = entityFactoryByFieldValue;
}
// =========================================================
// IMPLEMENTATION OF IEntityFactoryResolutionStrategy
// =========================================================
@NamespaceAccessible
public EntityFactory resolve(SObject record) {
final String fieldValue = (String) record.get(targetField);
if (!entityFactoryByFieldValue.containsKey(fieldValue)) {
throw new ConfigurationException(
'No EntityFactory for fieldValue ' +
fieldValue +
' defined on ' +
record.Id +
' for targetField: ' +
targetField.getDescribe().getName()
);
}
return entityFactoryByFieldValue.get(fieldValue);
}
@NamespaceAccessible
public void addEntityFactoryByFieldValue(String fieldValue, EntityFactory entityFactory) {
entityFactoryByFieldValue.put(fieldValue, entityFactory);
}
}
Then we can create one for our Stripe record type like so;
new FieldValueEntityFactoryStrategy(
Transaction__c.RecordTypeId,
new Map<String, EntityFactory> {
BaseTPTransaction.RT_TX_STRIPE_ID =>
new StripeTransactionEntity.Factory()
}
)
Then we can use some dependency injection to say that when I want to wrap a Transaction__c
record, we call Entity foo = fieldValueStrategy.resolve(record).wrap(record);
on the above strategy instance.
Money
When we want to generate a getter for a Currency field, we use a different freemarker template;
@NamespaceAccessible
public Money get${field.getName()}() {
return getConcreteRecord().${field.getAPIName()} != null ? new Money(getConcreteRecord().${field.getAPIName()}, 2) : null;
}
We love this, because over the years we’ve noticed some quirks with autoboxing and SFDC’s Decimal class. Let’s say we wanted to divide a number and maintain the scale for display or saving to the database later.
If you use the methods defined on Decimal you are good to go;
Decimal foo = 1.0;
System.debug(foo.divide(3.0, 2)); // Result: 0.33
But, if you rely on autoboxing, you’re going to lose your scale;
Decimal foo = 1.0;
Decimal bar = foo / 3; // Result: 0.333333333333333333333333333333333
This can actually be a big deal! We’ve seen values saved to much higher scales than the field is meant to accept - which can cause havoc when operating on currency values later.
With entities, we don’t ever deal with decimals directly, so autoboxing is impossible. Our Money class has a defined scale, and a user of it has to call getValue()
which ensures the correct scale is maintained.
@NamespaceAccessible
public void calculateSubmittedAmount() {
setSubmittedAmount(getSubtotalAmount().add(getSalesTaxAmount()).add(getSurchargeTotal()));
}
Quality of life getters
How often have you had to read a values from a picklist field and compare to a constant, and duplicate that same field comparison all over your code (or add a special utility method to define that comparison consistently across the code base)?
Being able to add a method directly to the entity makes this a piece of cake;
@NamespaceAccessible
public Boolean isImmutableStatus() {
return new Set<String> {
BaseTPTransaction.PL_TX_STATUS_APPROVED,
BaseTPTransaction.PL_TX_STATUS_CANCELLED,
BaseTPTransaction.PL_TX_STATUS_DECLINED,
BaseTPTransaction.PL_TX_STATUS_PARTIALAPPROVAL
}.contains(getStatus());
}
Now we have the business rule (what’s immutable / final) in a single method, in the right place, that is extremely accessible when working with any transaction record; tx.isImmutableStatus();
Accurately track changed fields
If you have all your generated entities extend a BaseEntity
class, and have all getters go via the superclass put
- you can very accurately track changed fields. We’ve made some changes to our UnitOfWork
class to hold references to entities and at commit time pull only the changed fields for the underlying record - so, a field that was changed as a result of put
on our base class - and only commit those changes.
Another side benefit of this is being able to intelligent figure out what to commit when you have multiple instances of the same record in memory, with different / conflicting sets of changes. Without this, whatever is committed last wins - even for fields that were not changed since that record was queried. Being able to interject every mutation allows us to merge conflicting changes later and commit a more sensible result to the database.
@NamespaceAccessible
public void put(String field, Object fieldValue) {
recordFieldChanged(record, field, fieldValue);
record.put(field, fieldValue);
}
@NamespaceAccessible
protected void recordFieldChanged(SObject record, String field, Object fieldVal) {
fieldChanges.addFieldChange(field, fieldVal);
}
and later in uow;
@NamespaceAccessible
public override void onCommitWorkStarting() {
mergeDirtyEntityChanges();
// ....
}
private void mergeDirtyEntityChanges() {
final Map<Id, MostRecentFieldChanges> changesById = new Map<Id, MostRecentFieldChanges>();
for(EntityRecordPair currentPair : dirtyEntityPairs) {
final MostRecentFieldChanges entityChanges = currentPair.entity.getFieldChanges();
if(!changesById.containsKey(currentPair.record.Id)) {
changesById.put(currentPair.record.Id, entityChanges);
} else {
changesById.get(currentPair.record.Id).mergeChanges(entityChanges);
}
// Once merged, clear changed fields on entity
currentPair.entity.clearChangedFields();
}
}
Lazy loading related records
There’s a few caveats to this one - you don’t want to break bulkability with this so you need to be selective about where you do it. For us it makes sense when dealing with;
- Custom metadata records
- Cached tables with a caching class structure that allows you to “warm it” in bulk.
Being able to lazily load related records has some great use cases - primarily if you don’t know if you’ll need the record when you query it. This can also be useful for cases where you use text ids instead of native lookups to avoid lookup skew (although we’ve since learned that can be quite the anti pattern, lookup skew isn’t as common as the docs may make you think.. that’s another post for another time)
In our Transaction, we have many related configuration objects that are custom metadata records, and it can be as simple as follows to add lazy loading support;
@NamespaceAccessible
public TPTenderTypeEntity getTenderTypeEntity() {
return TenderTypeRepository.getInstance().byId(getConcreteRecord().TX_TenderType_Id__c);
}
Summary
This is really just the tip of the iceberg. You can go further - we’ve actually removed all direct access to SObject record access in our code other than in a few select classes responsible for wrapping them into Entities. The vast majority of our code must deal with database records via our generated entity classes. It’s a total game changer.
Example Output
An example output entity for one of our smaller classes;
/**
* Generated class, do NOT edit.
*
* Your changes will be lost, and you are probably doing something wrong.
*
* ___________ ____
* ______/ \__// \__/____\
* _/ \_/ : //____\\
* /| : : .. / \
* | | :: :: \ /
* | | :| || \ \______/
* | | || || |\ / |
* \| || || | / | \
* | || || | / /_\ \
* | ___ || ___ || | / / \
* \_-_/ \_-_/ | ____ |/__/ \
* _\_--_/ \ /
* /____ /
* / \ /
* \______\_________/
*
* Generated @ Mar 13, 2022, 9:30:17 PM by Wrapper.cls.ftl
*
**/
@NamespaceAccessible
public with sharing abstract class BaseTPBatchRun extends BaseObjectEntity {
// =========================================================
// PICKLIST VALUES
// =========================================================
@NamespaceAccessible
public static final String PL_BR_STATUS_SUCCESS = 'Success';
@NamespaceAccessible
public static final String PL_BR_STATUS_IN_PROGRESS = 'In Progress';
@NamespaceAccessible
public static final String PL_BR_STATUS_FAILED = 'Failed';
@NamespaceAccessible
public static final String PL_BR_STATUS_PARTIAL = 'Partial';
// =========================================================
// CONSTRUCTORS
// =========================================================
@NamespaceAccessible
public BaseTPBatchRun(TP_BatchRun__c record) {
super(record);
}
// =========================================================
// PUBLIC METHODS
// =========================================================
@NamespaceAccessible
public String getName() {
return getConcreteRecord().Name;
}
@NamespaceAccessible
public Id getBatchChainRun() {
return getConcreteRecord().BR_BatchChainRun__c;
}
@NamespaceAccessible
public void setBatchChainRun(Id batchChainRun) {
put('BR_BatchChainRun__c', batchChainRun);
}
@NamespaceAccessible
public BaseTPBatchRun withBatchChainRun(Id batchChainRun) {
setBatchChainRun(batchChainRun);
return this;
}
@NamespaceAccessible
public void setBatchChainRun(BaseTPBatchChainRun batchChainRun, SObjectUnitOfWork uow) {
registerRelationshipTo(uow, TP_BatchRun__c.BR_BatchChainRun__c, batchChainRun);
}
@NamespaceAccessible
public BaseTPBatchRun withBatchChainRun(BaseTPBatchChainRun batchChainRun, SObjectUnitOfWork uow) {
setBatchChainRun(batchChainRun, uow);
return this;
}
@NamespaceAccessible
public String getBatchId() {
return getConcreteRecord().BR_Batch_Id__c;
}
@NamespaceAccessible
public void setBatchId(String batchId) {
put('BR_Batch_Id__c', batchId);
}
@NamespaceAccessible
public BaseTPBatchRun withBatchId(String batchId) {
setBatchId(batchId);
return this;
}
@NamespaceAccessible
public String getErrors() {
return getConcreteRecord().BR_Errors__c;
}
@NamespaceAccessible
public void setErrors(String errors) {
put('BR_Errors__c', errors);
}
@NamespaceAccessible
public BaseTPBatchRun withErrors(String errors) {
setErrors(errors);
return this;
}
@NamespaceAccessible
public String getStatus() {
return getConcreteRecord().BR_Status__c;
}
@NamespaceAccessible
public void setStatus(String status) {
put('BR_Status__c', status);
}
@NamespaceAccessible
public BaseTPBatchRun withStatus(String status) {
setStatus(status);
return this;
}
@NamespaceAccessible
public String getBatchClassName() {
return getConcreteRecord().BR_BatchClassName__c;
}
@NamespaceAccessible
public void setBatchClassName(String batchClassName) {
put('BR_BatchClassName__c', batchClassName);
}
@NamespaceAccessible
public BaseTPBatchRun withBatchClassName(String batchClassName) {
setBatchClassName(batchClassName);
return this;
}
// =========================================================
// PROTECTED METHODS
// =========================================================
@NamespaceAccessible
protected TP_BatchRun__c getConcreteRecord() {
return (TP_BatchRun__c) record;
}
}
The generated classes being abstract allows for extension and modification while still allowing new data model changes to be reflected in the generated classes.
Here’s our extending class;
@NamespaceAccessible
public with sharing class TPBatchRunEntity extends BaseTPBatchRun {
// =========================================================
// INNER CLASSES
// =========================================================
public with sharing class Factory extends EntityFactory implements IEntityFactoryResolutionStrategy {
public Factory() {
super(TP_BatchRun__c.SObjectType);
}
public override BaseEntity wrap(SObject record) {
return new TPBatchRunEntity((TP_BatchRun__c) record);
}
public EntityFactory resolve(SObject record) {
return this;
}
}
// =========================================================
// ATTRIBUTES
// =========================================================
// =========================================================
// CONSTRUCTORS
// =========================================================
@NamespaceAccessible
public TPBatchRunEntity(TP_BatchRun__c record) {
super(record);
}
// =========================================================
// PUBLIC METHODS
// =========================================================
@NamespaceAccessible
public DateTime getSystemModStamp() {
return getConcreteRecord().SystemModstamp;
}
@NamespaceAccessible
public TPBatchChainRunEntity getTPBatchChainRunEntity() {
if (getConcreteRecord().BR_BatchChainRun__c != null) {
return (TPBatchChainRunEntity) Application.getInstance().getEntity(getConcreteRecord().BR_BatchChainRun__r);
} else {
return null;
}
}
// =========================================================
// PRIVATE METHODS
// =========================================================
// =========================================================
// IMPLEMENTATION OF BaseEntity
// =========================================================
protected override void doValidate(List<ValidationError> errors) {
Validators.validateType(getBatchClassName(), TP_BatchRun__c.BR_BatchClassName__c.getDescribe().getLabel(), errors);
}
}
The meat of the logic happens in the FreeMarker template, here’s our example template for the entity class
<#ftl ns_prefixes={"D":"http://soap.sforce.com/2006/04/metadata"}>
<#-- @ftlvariable name="" type="com.tractionondemand.tpay2.build.domain.SObjectModel" -->
<#import "Wrapper.cls-macros.ftl" as h/>
<#include "GeneratedHeader.txt.ftl" />
@NamespaceAccessible
public with sharing abstract class Base${getClassName()} extends ${getExtendingClassName()} {
<#assign hasPicklistField>
<#list allFields as field>
<#if field.fieldXML.CustomField.type == "Picklist">
true
</#if>
</#list>
</#assign>
<#if hasPicklistField?contains("true")>
// =========================================================
// PICKLIST VALUES
// =========================================================
<#list allFields as field>
<#if field.fieldXML.CustomField.type == "Picklist">
<#list field.fieldXML.CustomField.valueSet.valueSetDefinition.value as value>
<#assign fieldFullName = value.fullName?replace("-", "")?replace(" ", "_")?replace("'", "")?replace("__", "_")?upper_case/>
@NamespaceAccessible
public static final String PL_${field.getAPIName()?upper_case?replace("__C", "")?replace("__", "_")}_${fieldFullName} = '${value.fullName?replace("\'" , "\\'")}';
</#list>
</#if>
</#list>
</#if>
<#if (getRecordTypeDeveloperNames()?size > 0)>
// =========================================================
// RECORD TYPES
// =========================================================
<#list getRecordTypeDeveloperNames() as currentRecordType>
@NamespaceAccessible
public static final Id RT_${currentRecordType?upper_case}_ID = ${getAPIName()}.SObjectType
.getDescribe()
.getRecordTypeInfosByDeveloperName()
.get('${getRecordTypeDeveloperNames()[0]}')
.recordTypeId;
</#list>
</#if>
// =========================================================
// CONSTRUCTORS
// =========================================================
@NamespaceAccessible
public Base${getClassName()}(${getAPIName()} record) {
super(record);
}
// =========================================================
// PUBLIC METHODS
// =========================================================
<#if (getRecordTypeDeveloperNames()?size > 0) && getExtendingClassName()?starts_with("Base")>
@NamespaceAccessible
public void setRecordTypeId(Id recordTypeId) {
put('RecordTypeId', recordTypeId);
}
@NamespaceAccessible
public Id getRecordTypeId() {
return getConcreteRecord().RecordTypeId;
}
</#if>
<#if nameField??>
@NamespaceAccessible
public String getName() {
return getConcreteRecord().Name;
}
<#if nameField.apexType != "AutoNumber">
@NamespaceAccessible
public void setName(String name) {
put('Name', name);
}
@NamespaceAccessible
public Base${getClassName()} withName(String name) {
setName(name);
return this;
}
</#if>
</#if>
<#list fields as field>
<#assign apexType><@h.getApexType fieldType="${field.fieldXML.CustomField.type}"/></#assign>
<#assign getter = .get_optional_template("Wrapper.cls-${apexType?lower_case}-getter.ftl")/>
<#if getter.exists>
<@getter.include />
<#else>
@NamespaceAccessible
public ${apexType} get${field.getName()}() {
return getConcreteRecord().${field.getAPIName()};
}
</#if>
<#if setters?seq_contains("${field.getAPIName()}")>
<#assign setter = .get_optional_template("Wrapper.cls-${apexType?lower_case}-setter.ftl")/>
<#if setter.exists>
<@setter.include />
<#else>
@NamespaceAccessible
public void set${field.getName()}(${apexType} ${field.getVariableName()}) {
put('${field.getAPIName()}', ${field.getVariableName()});
}
@NamespaceAccessible
public Base${getClassName()} with${field.getName()}(${apexType} ${field.getVariableName()}) {
set${field.getName()}(${field.getVariableName()});
return this;
}
</#if>
</#if>
</#list>
<#if isConcreteRecordGetter()>
// =========================================================
// PROTECTED METHODS
// =========================================================
@NamespaceAccessible
protected ${getAPIName()} getConcreteRecord() {
return (${getAPIName()}) record;
}
</#if>
}