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