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 of Decimal? (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. a DonationOpportunity class versus a ProductOpportunity class both extending a BaseOpportunity class where instantiation is determined at runtime based on the value of RecordTypeId - 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

  1. Find the object & field metadata
  2. 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();
        }
    }

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;

  1. Custom metadata records
  2. 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>

}