@@ -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