Skip to content

Commit e4a4934

Browse files
author
Tom Fuda
committed
interim commit of LightweightQueryField implementation. The core functionality is done. This implement some equivalent unit tests for LightweightQueryField.
1 parent 3287d43 commit e4a4934

File tree

3 files changed

+203
-72
lines changed

3 files changed

+203
-72
lines changed

fflib/src/classes/fflib_QueryFactory.cls

Lines changed: 126 additions & 40 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,9 @@ 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;
74+
private Boolean enforceFLS = false;
7575

76-
private Boolean sortSelectFields = true;
76+
private Boolean lightweight = false;
7777

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

8787
private QueryField getFieldToken(String fieldName){
88+
89+
// FLS will not be enforced, so we are going to take a lot of shortcuts in the name of performance
90+
if (this.lightweight) {
91+
return new LightweightQueryField(fieldName);
92+
}
93+
8894
QueryField result;
8995
if(!fieldName.contains('.')){ //single field
9096
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
@@ -130,15 +136,30 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
130136
return false;
131137
return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL();
132138
}
133-
139+
140+
/**
141+
* Construct a new fflib_QueryFactory instance, allowing you to use LightweightQueryFields
142+
* to build the query. This offers significant performance improvement in query build time
143+
* at the expense of FLS enforcement, and up-front field validation.
144+
* @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
145+
* @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query.
146+
**/
147+
public fflib_QueryFactory(Schema.SObjectType table, Boolean lightweight) {
148+
this(table);
149+
this.lightweight = lightweight;
150+
if (lightweight) {
151+
this.enforceFLS = false;
152+
}
153+
}
154+
134155
/**
135156
* Construct a new fflib_QueryFactory instance with no options other than the FROM caluse.
136157
* You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query.
137158
* @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
138159
**/
139160
public fflib_QueryFactory(Schema.SObjectType table){
140161
this.table = table;
141-
fields = new Set<QueryField>();
162+
fields = new List<QueryField>();
142163
order = new List<Ordering>();
143164
enforceFLS = false;
144165
}
@@ -171,6 +192,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
171192
* @param enforce whether to enforce field level security (read)
172193
**/
173194
public fflib_QueryFactory setEnforceFLS(Boolean enforce){
195+
if (this.lightweight && enforce) {
196+
throw new InvalidOperationException('Calling setEnforceFLS(true) on a "lightweight" QueryFactory instance is not allowed.');
197+
}
174198
this.enforceFLS = enforce;
175199
return this;
176200
}
@@ -179,10 +203,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
179203
* Sets a flag to indicate that this query should have ordered
180204
* query fields in the select statement (this at a small cost to performance).
181205
* If you are processing large query sets, you should switch this off.
206+
* @deprecated Fields are ALWAYS sorted within the generated SOQL, so this method now does nothing.
182207
* @param whether or not select fields should be sorted in the soql statement.
183208
**/
184209
public fflib_QueryFactory setSortSelectFields(Boolean doSort){
185-
this.sortSelectFields = doSort;
186210
return this;
187211
}
188212

@@ -192,7 +216,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
192216
* @param fieldName the API name of the field to add to the query's SELECT clause.
193217
**/
194218
public fflib_QueryFactory selectField(String fieldName){
195-
fields.add( getFieldToken(fieldName) );
219+
this.fields.add( getFieldToken(fieldName) );
196220
return this;
197221
}
198222
/**
@@ -206,31 +230,29 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
206230
throw new InvalidFieldException(null,this.table);
207231
if (enforceFLS)
208232
fflib_SecurityUtils.checkFieldIsReadable(table, field);
209-
fields.add( new QueryField(field) );
233+
if (lightweight)
234+
this.fields.add(new LightweightQueryField(field));
235+
else
236+
this.fields.add(new QueryField(field));
210237
return this;
211238
}
212239
/**
213240
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
214241
* @param fieldNames the Set of field API names to select.
215242
**/
216243
public fflib_QueryFactory selectFields(Set<String> fieldNames){
217-
List<String> fieldList = new List<String>();
218-
Set<QueryField> toAdd = new Set<QueryField>();
219244
for(String fieldName:fieldNames){
220-
toAdd.add( getFieldToken(fieldName) );
221-
}
222-
fields.addAll(toAdd);
245+
this.fields.add( getFieldToken(fieldName) );
246+
}
223247
return this;
224248
}
225249
/**
226250
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
227251
* @param fieldNames the List of field API names to select.
228252
**/
229253
public fflib_QueryFactory selectFields(List<String> fieldNames){
230-
Set<QueryField> toAdd = new Set<QueryField>();
231254
for(String fieldName:fieldNames)
232-
toAdd.add( getFieldToken(fieldName) );
233-
fields.addAll(toAdd);
255+
this.fields.add( getFieldToken(fieldName) );
234256
return this;
235257
}
236258
/**
@@ -244,7 +266,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
244266
throw new InvalidFieldException();
245267
if (enforceFLS)
246268
fflib_SecurityUtils.checkFieldIsReadable(table, token);
247-
this.fields.add( new QueryField(token) );
269+
if (lightweight)
270+
this.fields.add(new LightweightQueryField(token));
271+
else
272+
this.fields.add(new QueryField(token));
248273
}
249274
return this;
250275
}
@@ -259,7 +284,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
259284
throw new InvalidFieldException();
260285
if (enforceFLS)
261286
fflib_SecurityUtils.checkFieldIsReadable(table, token);
262-
this.fields.add( new QueryField(token) );
287+
if (lightweight)
288+
this.fields.add(new LightweightQueryField(token));
289+
else
290+
this.fields.add(new QueryField(token));
263291
}
264292
return this;
265293
}
@@ -326,12 +354,20 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
326354
}
327355

328356
/**
357+
* @deprecated Replaced by {@link #getSelectedFieldsAsList()}
329358
* @returns the selected fields
330359
**/
331360
public Set<QueryField> getSelectedFields() {
361+
return new Set<QueryField>(this.fields);
362+
}
363+
364+
/**
365+
* @returns the selected fields as a List<QueryField>
366+
**/
367+
public List<QueryField> getSelectedFieldsAsList() {
332368
return this.fields;
333369
}
334-
370+
335371
/**
336372
* Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
337373
* If not, a new one will be created and returned.
@@ -422,9 +458,6 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
422458

423459
fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship);
424460

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-
428461
if(assertIsAccessible){
429462
subSelectQuery.assertIsAccessible();
430463
}
@@ -501,7 +534,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
501534
**/
502535
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){
503536
order.add(
504-
new Ordering(new QueryField(field), direction, nullsLast)
537+
new Ordering(this.lightweight ? new LightweightQueryField(field) : new QueryField(field), direction, nullsLast)
505538
);
506539
return this;
507540
}
@@ -539,7 +572,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
539572
**/
540573
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){
541574
order.add(
542-
new Ordering(new QueryField(field), direction)
575+
new Ordering(this.lightweight ? new LightweightQueryField(field) : new QueryField(field), direction)
543576
);
544577
return this;
545578
}
@@ -554,15 +587,20 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
554587
if (fields.size() == 0){
555588
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
556589
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 + ', ';
590+
} else {
591+
// This bit of code de-dupes the list of QueryFields. Since we've moved away from using a Set to back this collection
592+
// (for performance reasons related to https://github.com/financialforcedev/fflib-apex-common/issues/79), we de-dupe
593+
// by first sorting the List of QueryField objects (in order of the String representation of the field path),
594+
// then making a pass through the list leaving dupes out of the "fieldsToQuery" collection.
595+
fields.sort(); // Sorts based on QueryFields's "comparable" implementation
596+
// Now that the QueryField list is sorted, we can de-dupe
597+
QueryField previousQf = null;
598+
for(QueryField field : fields){
599+
if (!field.equals(previousQf)) {
600+
result += field + ', ';
601+
}
602+
previousQf = field;
562603
}
563-
}else{
564-
for (QueryField field : fields)
565-
result += field + ', ';
566604
}
567605

568606
if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){
@@ -664,8 +702,53 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
664702
}
665703
}
666704

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

668-
public class QueryField implements Comparable{
751+
public virtual class QueryField implements Comparable{
669752
List<Schema.SObjectField> fields;
670753

671754
/**
@@ -681,7 +764,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
681764
public List<SObjectField> getFieldPath(){
682765
return fields.clone();
683766
}
684-
767+
768+
private QueryField() {}
769+
685770
@testVisible
686771
private QueryField(List<Schema.SObjectField> fields){
687772
if(fields == null || fields.size() == 0)
@@ -694,7 +779,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
694779
throw new InvalidFieldException('Invalid field: null');
695780
fields = new List<Schema.SObjectField>{ field };
696781
}
697-
public override String toString(){
782+
public virtual override String toString(){
698783
String result = '';
699784
Integer size = fields.size();
700785
for (Integer i=0; i<size; i++)
@@ -712,10 +797,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
712797
}
713798
return result;
714799
}
715-
public integer hashCode(){
800+
public virtual integer hashCode(){
716801
return String.valueOf(this.fields).hashCode();
717802
}
718-
public boolean equals(Object obj){
803+
public virtual boolean equals(Object obj){
719804
//Easy checks first
720805
if(obj == null || !(obj instanceof QueryField))
721806
return false;
@@ -744,7 +829,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
744829
* - QueryFields with more joins give +1, while fewer joins give -1
745830
* - For anything else, compare the toStrings of this and the supplied object.
746831
**/
747-
public Integer compareTo(Object o){
832+
public virtual Integer compareTo(Object o){
748833
if(o == null || !(o instanceof QueryField))
749834
return -2; //We can't possibly do a sane comparison against an unknwon type, go athead and let it "win"
750835

@@ -774,5 +859,6 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
774859
}
775860
public class InvalidFieldSetException extends Exception{}
776861
public class NonReferenceFieldException extends Exception{}
777-
public class InvalidSubqueryRelationshipException extends Exception{}
778-
}
862+
public class InvalidSubqueryRelationshipException extends Exception{}
863+
public class InvalidOperationException extends Exception{}
864+
}

0 commit comments

Comments
 (0)