5

I am looking for a good caching design pattern that is comprehensive enough to be used OOTB. This pattern would ultimately produce a central manager class that all classes/triggers used to get records from. A sample call would look like this:

List<SObject> records = CacheManager.getRecords(List<Id> recordIds)

The need for this arises from multiple classes modifying/accessing the same record within a single Apex execution before they are committed to database. Keep in mind these records may be retrieved as children of an object (using a sub-query), or lookups; e.g. modifying a child Contact record should be mirrored on the User.Contact lookup instance in memory. Pass-by-reference doesn't work here.

This design pattern should also work well with Workflows. If Workflows update a cached record it should have the means of getting the updated record. Obviously this is a simple SOQL call, but it's another puzzle piece which needs to be carefully facilitated by the framework.

I have my own ideas but I know there are plenty of small little things that I need to look out for. I am hoping I don't have to reinvent the wheel.

2
  • 2
    I actually wrote such a caching system a few years back to reduce the number of SOQL queries we had in triggers (one trigger was originally 96 SOQL calls, and my class reduced this to 56). It handled children records, too. I'll see if I can find a copy of it somewhere.
    – sfdcfox
    Commented Mar 22, 2016 at 20:27
  • 2
    Force.com Enterprise Architecture Selector Layer discusses caching on pg 217
    – cropredy
    Commented Mar 22, 2016 at 21:03

1 Answer 1

3

I made a SOQL query class that uses uses a caching mechanism. It is basically an improvement on an old setup I created a while back.

It is basically a version of the the fflib_QueryFactory.cls from FinancialForce's Apex Common (Nowhere near as robust and featured as Apex Common's but suitable for what I needed at the time). Main difference is, I wanted to reference an object instead of filling it in via String.

Basically, I wanted to write a where clause without doing this:

.setCondition(‘Name != “Bob”’)

I found a video using Apex design patterns (I forgot which one), specifically the Iterator pattern and decided to put it to use.

I broke the code up into seperate classes.

SOQLOperators

This class simple creates the conditional operators I need for my where clauses.

public with sharing class SOQLOperators 
{
    public enum ComparisonOperator
    {
        Equals,
        NotEquals,
        LessThan,
        LessOrEqual,
        GreaterThan,
        GreaterOrEqual,
        LikeComparison,
        StartsWith,
        InComparison,
        NotInComparison,
        IncludesComparison,
        ExcludesComparison
    }

    public static String GetComparisonOperatorString(ComparisonOperator comparingOperator)
    {
        if(ComparisonOperator.Equals == comparingOperator)
            return ' = ';
        else if (ComparisonOperator.NotEquals == comparingOperator)
            return ' != ';
        else if (ComparisonOperator.LessThan == comparingOperator)
            return ' < ';
        else if (ComparisonOperator.LessOrEqual == comparingOperator)
            return ' <= ';
        else if (ComparisonOperator.GreaterThan == comparingOperator)
            return ' > ';
        else if (ComparisonOperator.GreaterOrEqual == comparingOperator)
            return ' >= ';
        else if (ComparisonOperator.LikeComparison == comparingOperator)
            return ' LIKE ';
        else if (ComparisonOperator.StartsWith == comparingOperator)
            return ' LIKE ';
        else if (ComparisonOperator.InComparison == comparingOperator)
            return ' IN ';
        else if (ComparisonOperator.NotInComparison == comparingOperator)
            return' NOT IN ';
        else if (ComparisonOperator.IncludesComparison == comparingOperator)
            return ' INCLUDES ';
        else
            return ' EXCLUDES ';
    }
} 

SOQLExpressions

This class is where I store the expressions. This was the hardest part for me. Basically, this is where I store all my where clause information. Depending on the order I set things up, I can make some pretty complex conditions. From long AND expressions (great for filterable forms) to complex AND and OR logic. It then return a list of all the objects referenced in the entire WHERE clause for referencing later.

public with sharing class SOQLExpressions 
{
    public interface FieldExpression
    {
        FieldExpression AppendExpression(FieldExpression newFieldExpression);
        String ReturnExpression();
        List<Object> ReturnExpressionValues();
        FieldExpression SetBindingField(List<String> bindingFieldNames);
    }

    public abstract class ConditionalExpression implements FieldExpression
    {
        private List<FieldExpression> Expressions { get; set; }
        private List<FieldExpression> ChildExpressions { get; set; }

        public ConditionalExpression()
        {
            Expressions = new List<FieldExpression>();
            ChildExpressions = new List<FieldExpression>();
        }

        public FieldExpression AppendExpression(FieldExpression newFieldExpression)
        {
            if(newFieldExpression instanceof Expression)
                Expressions.add(newFieldExpression);
            else
                ChildExpressions.add(newFieldExpression);
            return this;
        }

        public FieldExpression SetBindingField(List<String> bindingFieldNames)
        {
            for(FieldExpression singleExpression : Expressions)
            {
                singleExpression.SetBindingField(bindingFieldNames);
                if(bindingFieldNames.size() > 0) 
                    bindingFieldNames.remove(0);
            }

            for(FieldExpression singleExpression : ChildExpressions)
            {
                singleExpression.SetBindingField(bindingFieldNames);
                if(bindingFieldNames.size() > 0) 
                    bindingFieldNames.remove(0);
            }

            return this;
        }

        public String ReturnExpression()
        {
            String completedExpression = ProduceExpressionString(Expressions);
            completedExpression += !ChildExpressions.isEmpty() ? ReturnLogicalExpression() + '(' + ProduceExpressionString(ChildExpressions) + ')' : '';
            return completedExpression;
        }

        public List<Object> ReturnExpressionValues()
        {
            List<Object> expressionValues = new List<Object>();

            for(FieldExpression singleExpression : Expressions)
                expressionValues.addAll(singleExpression.ReturnExpressionValues());

            for(FieldExpression singleExpression : ChildExpressions)
                expressionValues.addAll(singleExpression.ReturnExpressionValues());

            return expressionValues;
        }

        private String ProduceExpressionString(List<FieldExpression> Expressions)
        {
            String completedExpression = '';

            Integer expressionCount = 0;
            for(FieldExpression SingleExpression : Expressions)
                completedExpression += ((expressionCount++ != 0) ? ReturnLogicalExpression() : '') + SingleExpression.ReturnExpression();

            return CompletedExpression;
        }

        public abstract String ReturnLogicalExpression();
    }

    public class AndConditionalExpression extends ConditionalExpression
    {
        public override String ReturnLogicalExpression() { return ' AND '; }
    }

    public class OrConditionalExpression extends ConditionalExpression
    {
        public override String ReturnLogicalExpression() { return ' OR '; }
    }

    public class Expression implements FieldExpression
    {
        private String ExpressionFieldName { get; private set; }
        private SOQLOperators.ComparisonOperator ComparingOperator { get; private set; }
        public Object ExpressionValue { get; private set; }
        public String BindingField { get; private set; }

        public  Expression(String fieldName, SOQLOperators.ComparisonOperator comparisonOperator, Object comparisonValue)
        {
            ExpressionFieldName = fieldName;
            ComparingOperator = comparisonOperator;
            ExpressionValue = comparisonValue;
        }

        public FieldExpression SetBindingField(List<String> bindingFieldNames)
        {
            BindingField = bindingFieldNames[0];
            return this;
        }

        public FieldExpression AppendExpression(FieldExpression newFieldExpression) { return this; }

        public String ReturnExpression()
        {
            return ExpressionFieldName + SOQLOperators.GetComparisonOperatorString(ComparingOperator) + ':' + BindingField;
        }

        public List<Object> ReturnExpressionValues()
        {
            return new List<Object>{ ExpressionValue };
        }
    }
}

SOQLQuery

After all that, I simply created a query class that grabbed fields and the where clauses, arranging everything so it can be arranged and queried.

Since the limited scope of binded variables, I had to use argument placeholders for my query object. It worked out in the end though.

I also made an interface for the class for testing/mocking purposes.

public with sharing class SOQLQuery implements IQueryable
{
    public interface IQueryable { List<sObject> Execute(); }

    private Object Argument1, Argument2, Argument3, Argument4, Argument5, Argument6, Argument7, Argument8, Argument9, Argument10;
    private Object Argument11, Argument12, Argument13, Argument14, Argument15, Argument16, Argument17, Argument18, Argument19, Argument20;
    private SOQLExpressions.FieldExpression QueryExpression { get; set; }
    private Set<String> FieldsToQuery { get; set; }
    private String NameOfsObject { get; set; }
    private List<String> OrderByFieldsUsed { get; set; }
    private Integer LimitUsed { get; set; }

    public SOQLQuery(Set<String> queryFields, Schema.sObjectType sObjectToQuery, SOQLExpressions.FieldExpression newExpression)
    {
        this(queryFields, sObjectToQuery);
        QueryExpression = newExpression;
        AssignExpression(newExpression);
    }

    public SOQLQuery(Schema.sObjectType sObjectToQuery, SOQLExpressions.FieldExpression newExpression) 
    {
        this(sObjectToQuery.getDescribe().fields.getMap().keySet(), sObjectToQuery, newExpression);
    }

    public SOQLQuery(Schema.sObjectType sObjectToQuery) { this(sObjectToQuery.getDescribe().fields.getMap().keySet(), sObjectToQuery); }

    public SOQLQuery(Set<String> queryFields, Schema.sObjectType sObjectToQuery)
    {
        FieldsToQuery = queryFields;
        NameOfsObject = String.valueOf(sObjectToQuery);
        LimitUsed = 500;
    }

    public SOQLQuery(Schema.FieldSet fieldSet, Schema.sObjectType sObjectToQuery, SOQLExpressions.FieldExpression newExpression)
    {
        this(ConvertFieldSet(fieldSet), sObjectToQuery);
        QueryExpression = newExpression;
        AssignExpression(newExpression); 
    }

    public SOQLQuery(Schema.FieldSet fieldSet, Schema.sObjectType sObjectToQuery) { this(ConvertFieldSet(fieldSet), sObjectToQuery); }

    public SOQLQuery AssignExpression(SOQLExpressions.FieldExpression newExpression)
    {
        if(newExpression != null)
        {
            QueryExpression = newExpression;
            AssignArgumentsToFieldAndBindValues();
        } 

        return this;
    }

    public SOQLQuery AddExpression(SOQLExpressions.FieldExpression newExpression)
    {
        QueryExpression = (QueryExpression == null) ? newExpression : QueryExpression.AppendExpression(newExpression);
        AssignArgumentsToFieldAndBindValues(); 
        return this;
    }

    public SOQLQuery SetLimit(Integer newLimitToSet)
    { 
        LimitUsed = newLimitToSet; 
        return this;
    }

    public SOQLQuery SetOrderBy(List<String> newOrderByFields)
    {
        OrderByFieldsUsed = newOrderByFields;
        return this;
    }

    public List<sObject> Execute() 
    { 
        return (new SOQLQueryCacheManager()).ExecuteSOQLQueryObject(this);
    }

    public String QueryString()
    {
        String QueryToExecute = 'SELECT ' + String.join(new List<String>(FieldsToQuery), ', ') + ' FROM ' +  NameOfsObject 
            + (QueryExpression != null && String.isNotEmpty(QueryExpression.ReturnExpression()) ? ' WHERE ' + QueryExpression.ReturnExpression() : '')
            + (OrderByFieldsUsed != null ? ' OrderBy ' + String.join(OrderByFieldsUsed, ', ') : '')
            + ' LIMIT ' + LimitUsed;
        return QueryToExecute;
    }

    private static Set<String> ConvertFieldSet(Schema.FieldSet fieldSet)
    {
        Set<String> fieldsToUse = new Set<String>();
        for(Schema.FieldSetMember fieldForQuery : fieldSet.getFields())
            fieldsToUse.add(fieldForQuery.getFieldPath());
        return fieldsToUse;
    }

    private void AssignArgumentsToFieldAndBindValues()
    {
        List<Object> agruments = QueryExpression.ReturnExpressionValues();
        Integer numberOfArguments = agruments.size();

        if(numberOfArguments >= 1) Argument1 = agruments[0];
        if(numberOfArguments >= 2) Argument2 = agruments[1];
        if(numberOfArguments >= 3) Argument3 = agruments[2];
        if(numberOfArguments >= 4) Argument4 = agruments[3];
        if(numberOfArguments >= 5) Argument5 = agruments[4];

        if(numberOfArguments >= 6) Argument6 = agruments[5];
        if(numberOfArguments >= 7) Argument7 = agruments[6];
        if(numberOfArguments >= 8) Argument8 = agruments[7];
        if(numberOfArguments >= 9) Argument9 = agruments[8];
        if(numberOfArguments >= 10) Argument10 = agruments[9];

        if(numberOfArguments >= 11) Argument11 = agruments[10];
        if(numberOfArguments >= 12) Argument12 = agruments[11];
        if(numberOfArguments >= 13) Argument13 = agruments[12];
        if(numberOfArguments >= 14) Argument14 = agruments[13];
        if(numberOfArguments >= 15) Argument15 = agruments[14];

        if(numberOfArguments >= 16) Argument16 = agruments[15];
        if(numberOfArguments >= 17) Argument17 = agruments[16];
        if(numberOfArguments >= 18) Argument18 = agruments[17];
        if(numberOfArguments >= 19) Argument19 = agruments[18];
        if(numberOfArguments >= 20) Argument20 = agruments[19];

        List<String> bindingVariables = new List<String>();
        for(Integer argumentNumber = 1; argumentNumber < numberOfArguments + 1; argumentNumber++)
            bindingVariables.add('Argument' + argumentNumber);
        
        QueryExpression.SetBindingField(bindingVariables);
    }
}

SOQLQueryCacheManager

The second easiest class is taking care of the caching.

public with sharing class SOQLQueryCacheManager 
{
    private static Map<String, List<sObject>> CachedQueries = new Map<String, List<sObject>>();

    public List<sObject> ExecuteSOQLQueryObject(SOQLQuery queryToExecute)
    {
        if(!CachedQueries.containsKey(String.valueOf(queryToExecute)))
            CachedQueries.put(String.valueOf(queryToExecute), Database.Query(queryToExecute.QueryString()));

        return CachedQueries.get(String.valueOf(queryToExecute));
    }
}

So now you can retireve records anywhere, with one query, in multiple locations like this:

List<sObject> records = (new SOQLQuery(Example__c.sObjectType, 
   new SOQLExpressions.Expression('Id', SOQLOperators.ComparisonOperator.InComparison, recordIds))).Execute();

It seems like a lot of work, but the benefits more than surpass it.

I have been working to be more in-depth, but I've been lazy :P .

2
  • Appreciate taking time to post this. But, as far as I can see this is only able to cache query results. What I need is a cache of records (not cache of query results). I'll reiterate my use case: if Child__c was retrieved earlier and then modified by Apex, I want the framework to return the same instance of record if later on another method asked for it again.
    – Mossi
    Commented Jul 15, 2016 at 0:00
  • Ah, I see. Let me work on that a bit further. This sounds like an interesting challenge. Commented Jul 16, 2016 at 2:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .