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 .