Skip to content

Commit 4e5f8c2

Browse files
authored
Merge pull request apex-enterprise-patterns#108 from patronmanager/LightweightQueryField
Lightweight Query Factory
2 parents 1fd9b96 + 6165233 commit 4e5f8c2

File tree

3 files changed

+471
-89
lines changed

3 files changed

+471
-89
lines changed

fflib/src/classes/fflib_QueryFactory.cls

Lines changed: 147 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
6161
**/
6262
public Schema.SObjectType table {get; private set;}
6363
@testVisible
64-
private Set<QueryField> fields;
64+
private List<QueryField> fields;
6565
private String conditionExpression;
6666
private Integer limitCount;
6767
private Integer offset;
@@ -71,9 +71,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
7171
/* This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling
7272
/* one of the selectField or selectFieldset methods.
7373
**/
74-
private Boolean enforceFLS;
75-
76-
private Boolean sortSelectFields = true;
74+
@TestVisible
75+
private Boolean enforceFLS = false;
76+
@TestVisible
77+
private Boolean lightweight = false;
7778

7879
/**
7980
* The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to
@@ -85,6 +86,12 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
8586
private Map<Schema.ChildRelationship, fflib_QueryFactory> subselectQueryMap;
8687

8788
private QueryField getFieldToken(String fieldName){
89+
90+
// FLS will not be enforced, so we are going to take a lot of shortcuts in the name of performance
91+
if (this.lightweight) {
92+
return new LightweightQueryField(fieldName);
93+
}
94+
8895
QueryField result;
8996
if(!fieldName.contains('.')){ //single field
9097
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
@@ -130,19 +137,31 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
130137
return false;
131138
return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL();
132139
}
133-
140+
134141
/**
135142
* Construct a new fflib_QueryFactory instance with no options other than the FROM caluse.
136143
* You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query.
137144
* @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
138145
**/
139146
public fflib_QueryFactory(Schema.SObjectType table){
140-
this.table = table;
141-
fields = new Set<QueryField>();
142-
order = new List<Ordering>();
143-
enforceFLS = false;
147+
this(table, false);
144148
}
145149

150+
/**
151+
* Construct a new fflib_QueryFactory instance, allowing you to use LightweightQueryFields
152+
* to build the query. This offers significant performance improvement in query build time
153+
* at the expense of FLS enforcement, and up-front field validation.
154+
* @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
155+
* @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query.
156+
**/
157+
public fflib_QueryFactory(Schema.SObjectType table, Boolean lightweight) {
158+
this.table = table;
159+
this.fields = new List<QueryField>();
160+
this.order = new List<Ordering>();
161+
this.lightweight = lightweight;
162+
this.enforceFLS = false;
163+
}
164+
146165
/**
147166
* Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship.
148167
* This should be used when constructing a subquery query for addition to a parent query.
@@ -151,10 +170,22 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
151170
* @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}.
152171
**/
153172
private fflib_QueryFactory(Schema.ChildRelationship relationship){
154-
this(relationship.getChildSObject());
173+
this(relationship, false);
174+
}
175+
176+
/**
177+
* Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship.
178+
* This should be used when constructing a subquery query for addition to a parent query.
179+
* Objects created with this constructor cannot be added to another object using the subselectQuery method.
180+
* You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query.
181+
* @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}.
182+
* @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query.
183+
**/
184+
private fflib_QueryFactory(Schema.ChildRelationship relationship, Boolean lightweight){
185+
this(relationship.getChildSObject(), lightweight);
155186
this.relationship = relationship;
156187
}
157-
188+
158189
/**
159190
* This method checks to see if the User has Read Access on {@link #table}.
160191
* Asserts true if User has access.
@@ -171,6 +202,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
171202
* @param enforce whether to enforce field level security (read)
172203
**/
173204
public fflib_QueryFactory setEnforceFLS(Boolean enforce){
205+
if (this.lightweight && enforce) {
206+
throw new InvalidOperationException('Calling setEnforceFLS(true) on a "lightweight" QueryFactory instance is not allowed.');
207+
}
174208
this.enforceFLS = enforce;
175209
return this;
176210
}
@@ -179,10 +213,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
179213
* Sets a flag to indicate that this query should have ordered
180214
* query fields in the select statement (this at a small cost to performance).
181215
* If you are processing large query sets, you should switch this off.
216+
* @deprecated Fields are ALWAYS sorted within the generated SOQL, so this method now does nothing.
182217
* @param whether or not select fields should be sorted in the soql statement.
183218
**/
184219
public fflib_QueryFactory setSortSelectFields(Boolean doSort){
185-
this.sortSelectFields = doSort;
186220
return this;
187221
}
188222

@@ -192,7 +226,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
192226
* @param fieldName the API name of the field to add to the query's SELECT clause.
193227
**/
194228
public fflib_QueryFactory selectField(String fieldName){
195-
fields.add( getFieldToken(fieldName) );
229+
this.fields.add( getFieldToken(fieldName) );
196230
return this;
197231
}
198232
/**
@@ -206,31 +240,41 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
206240
throw new InvalidFieldException(null,this.table);
207241
if (enforceFLS)
208242
fflib_SecurityUtils.checkFieldIsReadable(table, field);
209-
fields.add( new QueryField(field) );
243+
this.fields.add(getQueryFieldFromToken(field));
210244
return this;
211245
}
246+
247+
/**
248+
* Returns the appropriate QueryField implementation, based on the "lightweight" flag
249+
* @param field the {@link Schema.SObjectField} for the QueryField
250+
* @returns either a QueryField, or LightweightQueryField object for the specified SObjectField
251+
**/
252+
private QueryField getQueryFieldFromToken(Schema.SObjectField field) {
253+
QueryField qf;
254+
if (this.lightweight)
255+
qf = new LightweightQueryField(field);
256+
else
257+
qf = new QueryField(field);
258+
return qf;
259+
}
260+
212261
/**
213262
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
214263
* @param fieldNames the Set of field API names to select.
215264
**/
216265
public fflib_QueryFactory selectFields(Set<String> fieldNames){
217-
List<String> fieldList = new List<String>();
218-
Set<QueryField> toAdd = new Set<QueryField>();
219266
for(String fieldName:fieldNames){
220-
toAdd.add( getFieldToken(fieldName) );
221-
}
222-
fields.addAll(toAdd);
267+
this.fields.add( getFieldToken(fieldName) );
268+
}
223269
return this;
224270
}
225271
/**
226272
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
227273
* @param fieldNames the List of field API names to select.
228274
**/
229275
public fflib_QueryFactory selectFields(List<String> fieldNames){
230-
Set<QueryField> toAdd = new Set<QueryField>();
231276
for(String fieldName:fieldNames)
232-
toAdd.add( getFieldToken(fieldName) );
233-
fields.addAll(toAdd);
277+
this.fields.add( getFieldToken(fieldName) );
234278
return this;
235279
}
236280
/**
@@ -243,8 +287,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
243287
if(token == null)
244288
throw new InvalidFieldException();
245289
if (enforceFLS)
246-
fflib_SecurityUtils.checkFieldIsReadable(table, token);
247-
this.fields.add( new QueryField(token) );
290+
fflib_SecurityUtils.checkFieldIsReadable(table, token);
291+
this.fields.add(getQueryFieldFromToken(token));
248292
}
249293
return this;
250294
}
@@ -258,8 +302,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
258302
if(token == null)
259303
throw new InvalidFieldException();
260304
if (enforceFLS)
261-
fflib_SecurityUtils.checkFieldIsReadable(table, token);
262-
this.fields.add( new QueryField(token) );
305+
fflib_SecurityUtils.checkFieldIsReadable(table, token);
306+
this.fields.add(getQueryFieldFromToken(token));
263307
}
264308
return this;
265309
}
@@ -326,12 +370,13 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
326370
}
327371

328372
/**
373+
* @deprecated Replaced by {@link #getSelectedFieldsAsList()}
329374
* @returns the selected fields
330375
**/
331376
public Set<QueryField> getSelectedFields() {
332-
return this.fields;
377+
return new Set<QueryField>(this.fields);
333378
}
334-
379+
335380
/**
336381
* Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
337382
* If not, a new one will be created and returned.
@@ -420,11 +465,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
420465
return subselectQueryMap.get(relationship);
421466
}
422467

423-
fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship);
468+
fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship, this.lightweight);
424469

425-
//The child queryFactory should be configured in the same way as the parent by default - can override after if required
426-
subSelectQuery.setSortSelectFields(sortSelectFields);
427-
428470
if(assertIsAccessible){
429471
subSelectQuery.assertIsAccessible();
430472
}
@@ -501,7 +543,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
501543
**/
502544
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){
503545
order.add(
504-
new Ordering(new QueryField(field), direction, nullsLast)
546+
new Ordering(getQueryFieldFromToken(field), direction, nullsLast)
505547
);
506548
return this;
507549
}
@@ -539,7 +581,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
539581
**/
540582
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){
541583
order.add(
542-
new Ordering(new QueryField(field), direction)
584+
new Ordering(getQueryFieldFromToken(field), direction)
543585
);
544586
return this;
545587
}
@@ -554,15 +596,20 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
554596
if (fields.size() == 0){
555597
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
556598
result += 'Id ';
557-
}else if(sortSelectFields){
558-
List<QueryField> fieldsToQuery = new List<QueryField>(fields);
559-
fieldsToQuery.sort(); //delegates to QueryFilter's comparable implementation
560-
for(QueryField field:fieldsToQuery){
561-
result += field + ', ';
599+
} else {
600+
// This bit of code de-dupes the list of QueryFields. Since we've moved away from using a Set to back this collection
601+
// (for performance reasons related to https://github.com/financialforcedev/fflib-apex-common/issues/79), we de-dupe
602+
// by first sorting the List of QueryField objects (in order of the String representation of the field path),
603+
// then making a pass through the list leaving dupes out of the "fieldsToQuery" collection.
604+
fields.sort(); // Sorts based on QueryFields's "comparable" implementation
605+
// Now that the QueryField list is sorted, we can de-dupe
606+
QueryField previousQf = null;
607+
for(QueryField field : fields){
608+
if (!field.equals(previousQf)) {
609+
result += field + ', ';
610+
}
611+
previousQf = field;
562612
}
563-
}else{
564-
for (QueryField field : fields)
565-
result += field + ', ';
566613
}
567614

568615
if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){
@@ -592,7 +639,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
592639
**/
593640
public fflib_QueryFactory deepClone(){
594641

595-
fflib_QueryFactory clone = new fflib_QueryFactory(this.table)
642+
fflib_QueryFactory clone = new fflib_QueryFactory(this.table, this.lightweight)
596643
.setLimit(this.limitCount)
597644
.setCondition(this.conditionExpression)
598645
.setEnforceFLS(this.enforceFLS);
@@ -664,8 +711,54 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
664711
}
665712
}
666713

714+
public class LightweightQueryField extends QueryField implements Comparable {
715+
String fieldName;
716+
717+
private LightweightQueryField() {}
718+
719+
@TestVisible
720+
private LightweightQueryField(String fieldName) {
721+
// Convert strings to lowercase so they sort in a case insensitive manner
722+
this.fieldName = fieldName.toLowercase();
723+
}
724+
725+
@TestVisible
726+
private LightweightQueryField(Schema.SObjectField field) {
727+
// Convert strings to lowercase so they sort in a case insensitive manner
728+
this.fieldName = field.getDescribe().getLocalName().toLowercase();
729+
}
730+
731+
public override String toString() { return this.fieldName; }
732+
733+
public override Integer hashCode() {
734+
return (this.fieldName == null) ? 0 : this.fieldName.hashCode();
735+
}
736+
737+
public override Boolean equals(Object obj) {
738+
return ((obj != null)
739+
&& (obj instanceof LightweightQueryField)
740+
&& (this.fieldName == ((LightweightQueryField) obj).fieldName));
741+
}
742+
743+
public override Integer compareTo(Object obj) {
744+
if (obj == null || !(obj instanceof LightweightQueryField))
745+
return 1;
746+
747+
if (this.fieldName == null) {
748+
if (((LightweightQueryField) obj).fieldName == null)
749+
// Both objects are non-null, but their fieldName is null
750+
return 0;
751+
else
752+
// Our fieldName is null, but theirs isn't
753+
return -1;
754+
}
755+
756+
// Both objects have non-null fieldNames, so just return the result of String.compareTo
757+
return this.fieldName.compareTo(((LightweightQueryField) obj).fieldName);
758+
}
759+
}
667760

668-
public class QueryField implements Comparable{
761+
public virtual class QueryField implements Comparable{
669762
List<Schema.SObjectField> fields;
670763

671764
/**
@@ -681,7 +774,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
681774
public List<SObjectField> getFieldPath(){
682775
return fields.clone();
683776
}
684-
777+
778+
private QueryField() {}
779+
685780
@testVisible
686781
private QueryField(List<Schema.SObjectField> fields){
687782
if(fields == null || fields.size() == 0)
@@ -694,7 +789,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
694789
throw new InvalidFieldException('Invalid field: null');
695790
fields = new List<Schema.SObjectField>{ field };
696791
}
697-
public override String toString(){
792+
public virtual override String toString(){
698793
String result = '';
699794
Integer size = fields.size();
700795
for (Integer i=0; i<size; i++)
@@ -712,10 +807,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
712807
}
713808
return result;
714809
}
715-
public integer hashCode(){
810+
public virtual integer hashCode(){
716811
return String.valueOf(this.fields).hashCode();
717812
}
718-
public boolean equals(Object obj){
813+
public virtual boolean equals(Object obj){
719814
//Easy checks first
720815
if(obj == null || !(obj instanceof QueryField))
721816
return false;
@@ -744,7 +839,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
744839
* - QueryFields with more joins give +1, while fewer joins give -1
745840
* - For anything else, compare the toStrings of this and the supplied object.
746841
**/
747-
public Integer compareTo(Object o){
842+
public virtual Integer compareTo(Object o){
748843
if(o == null || !(o instanceof QueryField))
749844
return -2; //We can't possibly do a sane comparison against an unknwon type, go athead and let it "win"
750845

@@ -774,5 +869,6 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
774869
}
775870
public class InvalidFieldSetException extends Exception{}
776871
public class NonReferenceFieldException extends Exception{}
777-
public class InvalidSubqueryRelationshipException extends Exception{}
778-
}
872+
public class InvalidSubqueryRelationshipException extends Exception{}
873+
public class InvalidOperationException extends Exception{}
874+
}

0 commit comments

Comments
 (0)