Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

Commit 69ac232

Browse files
jongpiejamessimone
authored andcommitted
Queries can now be cached with cacheResults() (#76)
* Queries generated by a query builder class can now be cached w/ cacheResults() * Deleted some demo code in QueryBuilder.cls * Added caching to AggregateResultQueryBuilder.cls * Added a caching test to AggregateResultQueryBuilder_Tests.cls * Stupid missing line break * Added some additional query builder tests * Reverted a method name change * Added exclude_paths to .codeclimate.yml so test classes aren't checked * Updated the code climate exclude_paths value
1 parent 2948ab2 commit 69ac232

13 files changed

+210
-34
lines changed

.codeclimate.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ engines:
33
enabled: true
44
config:
55
rulesets: "./tools/codeclimate/apex/apexunit.xml,./tools/codeclimate/apex/complexity.xml,./tools/codeclimate/apex/performance.xml,./tools/codeclimate/apex/security.xml,./tools/codeclimate/apex/style.xml"
6-
enabled: true
6+
exclude_paths:
7+
- "**Tests.cls"
78
ratings:
89
paths:
910
- "**.cls"

src/classes/AggregateResultQueryBuilder.cls

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public class AggregateResultQueryBuilder extends QueryBuilder implements IAggreg
2323
this.groupByList = new List<String>();
2424
}
2525

26+
public IAggregateResultQueryBuilder cacheResults() {
27+
super.doCacheResults();
28+
return this;
29+
}
30+
2631
public IAggregateResultQueryBuilder groupBy(IQueryField groupByQueryField) {
2732
return this.groupBy(new List<IQueryField>{groupByQueryField});
2833
}

src/classes/AggregateResultQueryBuilder_Tests.cls

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@
55
@isTest
66
private class AggregateResultQueryBuilder_Tests {
77

8+
9+
@isTest
10+
static void it_should_be_usable_after_construction() {
11+
// Query builders should be usable as soon as it's constructed - it should be able to execute a query with some default values
12+
IAggregateResultQueryBuilder aggregateQueryBuilder = new AggregateResultQueryBuilder(Schema.Opportunity.SObjectType);
13+
14+
Test.startTest();
15+
16+
List<AggregateResult> results = (List<AggregateResult>)aggregateQueryBuilder.getQueryResults();
17+
18+
Test.stopTest();
19+
}
20+
21+
@isTest
22+
static void it_should_cache_results() {
23+
IAggregateResultQueryBuilder aggregateResultQueryBuilder = new AggregateResultQueryBuilder(Schema.Opportunity.SObjectType);
24+
aggregateResultQueryBuilder.cacheResults();
25+
26+
Test.startTest();
27+
28+
System.assertEquals(0, Limits.getQueries());
29+
for(Integer i = 0; i < 10; i++) {
30+
System.debug(aggregateResultQueryBuilder.getQueryResults());
31+
}
32+
33+
System.assertEquals(1, Limits.getQueries());
34+
35+
Test.stopTest();
36+
}
37+
838
@isTest
939
static void it_should_build_a_ridiculous_query_string() {
1040
String expectedString = 'SELECT Type,\nAVG(Amount) AVG__Amount, COUNT(AccountId) COUNT__AccountId, '

src/classes/IAggregateResultQueryBuilder.cls

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
public interface IAggregateResultQueryBuilder {
1414

15+
IAggregateResultQueryBuilder cacheResults();
16+
1517
// Group By methods
1618
IAggregateResultQueryBuilder groupBy(IQueryField groupByQueryField);
1719
IAggregateResultQueryBuilder groupBy(List<IQueryField> groupByQueryFields);

src/classes/ISObjectQueryBuilder.cls

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
public interface ISObjectQueryBuilder {
1414

15+
ISObjectQueryBuilder cacheResults();
16+
1517
// Field methods
1618
ISObjectQueryBuilder addAllFields();
1719
ISObjectQueryBuilder addAllStandardFields();

src/classes/ISearchQueryBuilder.cls

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
public interface ISearchQueryBuilder {
22

3+
ISearchQueryBuilder cacheResults();
4+
35
ISearchQueryBuilder setQuerySearchGroup(QuerySearchGroup searchGroup);
46

57
String getQuery();

src/classes/QueryBuilder.cls

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,28 @@
1212
*/
1313
public abstract class QueryBuilder extends NebulaCore {
1414

15+
private static Map<Integer, List<SObject>> cachedQueryResultsByHashCode = new Map<Integer, List<SObject>>();
16+
private static Map<Integer, List<List<SObject>>> cachedSearchResultsByHashCode = new Map<Integer, List<List<SObject>>>();
17+
1518
protected List<String> whereClauseList;
1619
protected List<String> orderByList;
1720
protected Integer limitCount;
1821

1922
protected SObjectType sobjectType;
2023
protected Map<String, Schema.SObjectField> sobjectTypeFieldMap;
2124

25+
private Boolean cacheResults;
26+
2227
public QueryBuilder() {
2328
this.currentModule = NebulaCore.Module.QUERY_BUILDER;
2429

25-
this.whereClauseList = new List<String>();
26-
this.orderByList = new List<String>();
30+
this.whereClauseList = new List<String>();
31+
this.orderByList = new List<String>();
32+
this.cacheResults = false;
33+
}
34+
35+
protected void doCacheResults() {
36+
this.cacheResults = true;
2737
}
2838

2939
protected void doFilterBy(IQueryFilter queryFilter) {
@@ -83,19 +93,13 @@ public abstract class QueryBuilder extends NebulaCore {
8393
}
8494

8595
protected List<SObject> doGetQueryResults(String query) {
86-
List<SObject> results = Database.query(query);
87-
88-
this.logResults(query, results);
89-
90-
return results;
96+
if(this.cacheResults) return this.getCachedQuery(query);
97+
else return this.executeQuery(query);
9198
}
9299

93100
protected List<List<SObject>> doGetSearchResults(String query) {
94-
List<List<SObject>> results = Search.query(query);
95-
96-
this.logResults(query, results);
97-
98-
return results;
101+
if(this.cacheResults) return this.getCachedSearch(query);
102+
else return this.executeSearch(query);
99103
}
100104

101105
private void filterByWithSeparator(List<IQueryFilter> queryFilters, String separator) {
@@ -108,6 +112,41 @@ public abstract class QueryBuilder extends NebulaCore {
108112
this.whereClauseList.add(orStatement);
109113
}
110114

115+
private List<SObject> getCachedQuery(String query) {
116+
Integer hashCode = query.hashCode();
117+
118+
Boolean isCached = cachedQueryResultsByHashCode.containsKey(hashCode);
119+
if(!isCached) cachedQueryResultsByHashCode.put(hashCode, this.executeQuery(query));
120+
121+
// Always return a deep clone so the original cached version is never modified
122+
return cachedQueryResultsByHashCode.get(hashCode).deepClone(true, true, true);
123+
}
124+
125+
private List<SObject> executeQuery(String query) {
126+
List<SObject> results = Database.query(query);
127+
this.logResults(query, results);
128+
return results;
129+
}
130+
131+
private List<List<SObject>> getCachedSearch(String query) {
132+
Integer hashCode = query.hashCode();
133+
134+
Boolean isCached = cachedSearchResultsByHashCode.containsKey(hashCode);
135+
if(!isCached) cachedSearchResultsByHashCode.put(hashCode, this.executeSearch(query));
136+
137+
// Always return a deep clone so the original cached version is never modified
138+
List<List<SObject>> cachedResults = cachedSearchResultsByHashCode.get(hashCode);
139+
List<List<SObject>> deepClonedResults = new List<List<SObject>>();
140+
for(List<SObject> cachedListOfResults : cachedResults) deepClonedResults.add(cachedListOfResults.deepClone(true, true, true));
141+
return deepClonedResults;
142+
}
143+
144+
private List<List<SObject>> executeSearch(String query) {
145+
List<List<SObject>> results = Search.query(query);
146+
this.logResults(query, results);
147+
return results;
148+
}
149+
111150
private void logResults(String query, List<Object> results) {
112151
String logEntry = 'Query:\n' + query + '\n\nResults:\n' + JSON.serializePretty(results);
113152
Logger.addEntry(this, logEntry);

src/classes/SObjectQueryBuilder.cls

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public class SObjectQueryBuilder extends QueryBuilder implements ISObjectQueryBu
3030
this.addCommonQueryFields();
3131
}
3232

33+
public ISObjectQueryBuilder cacheResults() {
34+
super.doCacheResults();
35+
return this;
36+
}
37+
3338
public ISObjectQueryBuilder addFields(List<IQueryField> queryFields) {
3439
for(IQueryField queryField : queryFields) this.addField(queryField);
3540
return this;

src/classes/SObjectQueryBuilder_Tests.cls

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ private class SObjectQueryBuilder_Tests {
5959
return queryFields;
6060
}
6161

62+
@isTest
63+
static void it_should_be_usable_after_construction() {
64+
// Query builders should be usable as soon as it's constructed - it should be able to execute a query with some default values
65+
ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType);
66+
67+
Test.startTest();
68+
69+
List<Opportunity> results = (List<Opportunity>)opportunityQueryBuilder.getQueryResults();
70+
71+
Test.stopTest();
72+
}
73+
74+
@isTest
75+
static void it_should_cache_results() {
76+
ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType);
77+
opportunityQueryBuilder.cacheResults();
78+
79+
Test.startTest();
80+
81+
System.assertEquals(0, Limits.getQueries());
82+
for(Integer i = 0; i < 10; i++) {
83+
System.debug(opportunityQueryBuilder.getQueryResults());
84+
}
85+
86+
System.assertEquals(1, Limits.getQueries());
87+
88+
Test.stopTest();
89+
}
90+
6291
@isTest
6392
static void it_should_add_a_list_of_fields() {
6493
NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false;

src/classes/SObjectRecordTypes.cls

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,18 @@
1212
*/
1313
public abstract class SObjectRecordTypes extends NebulaCore implements ISObjectRecordTypes {
1414

15-
private static Map<String, List<RecordType>> cachedRecordTypesBySObjectMap = new Map<String, List<RecordType>>();
16-
1715
private String sobjectName;
1816

1917
public SObjectRecordTypes() {
2018
this.currentModule = NebulaCore.Module.RECORD_TYPES;
2119

2220
this.sobjectName = this.getSObjectType().getDescribe().getName();
23-
24-
Logger.addEntry(this, 'Getting record types for ' + this.sobjectName);
25-
26-
this.populateCache();
2721
}
2822

2923
public abstract Schema.SObjectType getSObjectType();
3024

3125
public Map<Id, RecordType> getAllById() {
32-
return new Map<Id, RecordType>(cachedRecordTypesBySObjectMap.get(this.sobjectName));
26+
return new Map<Id, RecordType>(getRecordTypes());
3327
}
3428

3529
public Map<String, RecordType> getAllByDeveloperName() {
@@ -43,14 +37,9 @@ public abstract class SObjectRecordTypes extends NebulaCore implements ISObjectR
4337
return allRecordTypesByDeveloperName;
4438
}
4539

46-
// TODO clean up all this mess for cache, getRecordTypes, getAllRecordTypesByDeveloperName, etc
47-
// Too many similarly named methods going on
48-
private void populateCache() {
49-
if(cachedRecordTypesBySObjectMap.containsKey(this.sobjectName)) return;
50-
51-
// Trying to call Schema.RecordType.SObjectType confuses Apex, so we have to get it through an extra describe call
40+
private List<RecordType> getRecordTypes() {
5241
Schema.SObjectType recordTypeSObjectType = Schema.SObjectType.RecordType.getSObjectType();
53-
ISObjectQueryBuilder query = new SObjectQueryBuilder(recordTypeSObjectType);
42+
ISObjectQueryBuilder query = new SObjectQueryBuilder(recordTypeSObjectType).orderBy(new QueryField(Schema.RecordType.DeveloperName));
5443

5544
// If we don't have the SObject cached, then we need to query
5645
if(NebulaSettings.RecordTypesSettings.LazyLoad__c) {
@@ -63,15 +52,9 @@ public abstract class SObjectRecordTypes extends NebulaCore implements ISObjectR
6352
query.filterBy(new QueryFilter().filterByField(new QueryField(Schema.RecordType.NamespacePrefix), QueryOperator.EQUALS, null));
6453
}
6554

66-
query.orderBy(new QueryField(Schema.RecordType.DeveloperName));
67-
6855
Logger.addEntry(this, 'Loading SObjectRecordTypes for=' + this.getSObjectType());
6956

70-
if(!cachedRecordTypesBySObjectMap.containsKey(this.sobjectName)) cachedRecordTypesBySObjectMap.put(this.sobjectName, new List<RecordType>());
71-
List<RecordType> recordTypeList = (List<RecordType>)query.getQueryResults();
72-
for(RecordType recordType : recordTypeList) cachedRecordTypesBySObjectMap.get(this.sobjectName).add(recordType);
73-
74-
Logger.addEntry(this, 'cachedRecordTypesBySObjectMap=' + cachedRecordTypesBySObjectMap);
57+
return (List<RecordType>)query.cacheResults().getQueryResults();
7558
}
7659

7760
}

0 commit comments

Comments
 (0)