Skip to content

Commit d793136

Browse files
committed
Merge pull request apex-enterprise-patterns#99 from financialforcedev/selector-and-query-factory-optimisations
Selector and query factory optimisations
2 parents 6bcdb4b + 3c84b61 commit d793136

File tree

4 files changed

+160
-39
lines changed

4 files changed

+160
-39
lines changed

fflib/src/classes/fflib_QueryFactory.cls

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
7373
**/
7474
private Boolean enforceFLS;
7575

76+
private Boolean sortSelectFields = true;
77+
7678
/**
7779
* The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to
7880
* a query, as long as it isn't a subselect query itself. You may have many subselects inside
@@ -173,6 +175,17 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
173175
return this;
174176
}
175177

178+
/**
179+
* Sets a flag to indicate that this query should have ordered
180+
* query fields in the select statement (this at a small cost to performance).
181+
* If you are processing large query sets, you should switch this off.
182+
* @param whether or not select fields should be sorted in the soql statement.
183+
**/
184+
public fflib_QueryFactory setSortSelectFields(Boolean doSort){
185+
this.sortSelectFields = doSort;
186+
return this;
187+
}
188+
176189
/**
177190
* Selects a single field from the SObject specified in {@link #table}.
178191
* Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact.
@@ -408,6 +421,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
408421
}
409422

410423
fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship);
424+
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+
411428
if(assertIsAccessible){
412429
subSelectQuery.assertIsAccessible();
413430
}
@@ -537,13 +554,17 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
537554
if (fields.size() == 0){
538555
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
539556
result += 'Id ';
540-
}else{
557+
}else if(sortSelectFields){
541558
List<QueryField> fieldsToQuery = new List<QueryField>(fields);
542559
fieldsToQuery.sort(); //delegates to QueryFilter's comparable implementation
543560
for(QueryField field:fieldsToQuery){
544561
result += field + ', ';
545562
}
546-
}
563+
}else{
564+
for (QueryField field : fields)
565+
result += field + ', ';
566+
}
567+
547568
if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){
548569
for (fflib_QueryFactory childRow : subselectQueryMap.values()){
549570
result += ' (' + childRow.toSOQL() + '), ';
@@ -675,57 +696,70 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
675696
}
676697
public override String toString(){
677698
String result = '';
678-
Iterator<Schema.sObjectField> i = fields.iterator();
679-
while(i.hasNext()){
680-
String fieldName = i.next().getDescribe().getName();
681-
if(fieldName.endsWithIgnoreCase('Id') && i.hasNext())
682-
fieldName = fieldName.removeEndIgnoreCase('Id');
683-
if(fieldName.endsWithIgnoreCase('__c') && i.hasNext())
684-
fieldName = fieldName.removeEndIgnoreCase('__c')+'__r';
685-
result += fieldName + (i.hasNext() ? '.' :'');
699+
Integer size = fields.size();
700+
for (Integer i=0; i<size; i++)
701+
{
702+
if (i>0)
703+
{
704+
if (result.endsWithIgnoreCase('Id'))
705+
result = result.removeEndIgnoreCase('Id');
706+
else if (result.endsWithIgnoreCase('__c')) {
707+
result = result.removeEndIgnoreCase('__c') + '__r';
708+
}
709+
result += '.';
710+
}
711+
result += fields[i].getDescribe().getName();
686712
}
687713
return result;
688714
}
689715
public integer hashCode(){
690716
return String.valueOf(this.fields).hashCode();
691717
}
692718
public boolean equals(Object obj){
693-
if(!(obj instanceof QueryField))
719+
//Easy checks first
720+
if(obj == null || !(obj instanceof QueryField))
694721
return false;
695-
if( String.valueOf(((QueryField) obj).fields) != String.valueOf(this.fields))
722+
723+
if (this === obj)
724+
return true;
725+
726+
//Detailed checks
727+
QueryField other = (QueryField)obj;
728+
Integer size = fields.size();
729+
if (size != other.fields.size())
696730
return false;
697-
Set<Schema.SObjectField> objFields = new Set<Schema.SObjectField>();
698-
objFields.addAll( ((QueryField)obj).fields );
699-
objFields.retainAll(this.fields);
700-
objFields.removeAll(this.fields);
701-
return objFields.size() == 0;
731+
732+
for (Integer i=0; i<size; i++)
733+
if (fields[i] != (other.fields[i]))
734+
return false;
735+
736+
return true;
702737
}
703738
/**
704-
* Allows sorting QueryField instances, which means we'll get deterministic field ordering by just sorting the parent
705-
* QueryFactory's array when toSOQL'ing.
739+
* Allows sorting QueryField instances.
706740
*
707741
* Returns:
708742
* - Objects that are not QueryField instances as -2, which functions as -1 but with more flair
709-
* - QueryField instances with less joins in their path as -1
710-
* - QueryField instances with an equal number of joins and alphabetically first as an undefined negative integer
711-
* - equals as 0
712-
* - anything else an undefined positive integer (usually, but not always 1)
743+
* - Equivalent QueryFields return 0.
744+
* - QueryFields with more joins give +1, while fewer joins give -1
745+
* - For anything else, compare the toStrings of this and the supplied object.
713746
**/
714747
public Integer compareTo(Object o){
715-
if(!(o instanceof QueryField))
748+
if(o == null || !(o instanceof QueryField))
716749
return -2; //We can't possibly do a sane comparison against an unknwon type, go athead and let it "win"
717-
QueryField that = (QueryField) o;
718-
if(this.fields.size() < that.fields.size()){
750+
751+
if (this === o)
752+
return 0;
753+
754+
QueryField other = (QueryField)o;
755+
Integer size = fields.size();
756+
Integer sizeOther = other.fields.size();
757+
if (size < sizeOther)
719758
return -1;
720-
}else if( this.fields.size() == that.fields.size() ){
721-
if(this.equals(that)){
722-
return 0;
723-
}else{
724-
return this.toString().compareTo(that.toString());
725-
}
726-
}else{
759+
if (size > sizeOther)
727760
return 1;
728-
}
761+
762+
return this.toString().compareTo(other.toString());
729763
}
730764
}
731765

fflib/src/classes/fflib_QueryFactoryTest.cls

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ private class fflib_QueryFactoryTest {
630630
User.SObjectType.fields.Name
631631
});
632632
System.assertEquals(-2, qf.compareTo(otherType));
633+
System.assertEquals(-2, qf.compareTo(null));
633634
System.assertEquals(0, qf.compareTo(qf));
634635
System.assertEquals(
635636
0,
@@ -774,6 +775,35 @@ private class fflib_QueryFactoryTest {
774775
System.assert(qf.getSubselectQueries().get(0).getOrderings().size() == 0);
775776
System.assert(qf2.getSubselectQueries().get(0).getOrderings().size() == 1);
776777
}
778+
779+
@isTest
780+
static void testSoql_unsortedSelectFields(){
781+
//Given
782+
fflib_QueryFactory qf = new fflib_QueryFactory(User.SObjectType);
783+
qf.selectFields(new List<String>{
784+
'Id',
785+
'FirstName',
786+
'LastName',
787+
'CreatedBy.Name',
788+
'CreatedBy.Manager',
789+
'LastModifiedBy.Email'
790+
});
791+
792+
qf.setSortSelectFields(false);
793+
794+
String orderedQuery =
795+
'SELECT '
796+
+'FirstName, Id, LastName, ' //less joins come first, alphabetically
797+
+'CreatedBy.ManagerId, CreatedBy.Name, LastModifiedBy.Email ' //alphabetical on the same number of joins'
798+
+'FROM User';
799+
800+
//When
801+
String actualSoql = qf.toSOQL();
802+
803+
//Then
804+
System.assertNotEquals(orderedQuery, actualSoql);
805+
}
806+
777807

778808
public static User createTestUser_noAccess(){
779809
User usr;

fflib/src/classes/fflib_SObjectSelector.cls

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public abstract with sharing class fflib_SObjectSelector
5959
* Order by field
6060
**/
6161
private String m_orderBy;
62+
63+
/**
64+
* Sort the query fields in the select statement (defaults to true, at the expense of performance).
65+
* Switch this off if you need more performant queries.
66+
**/
67+
private Boolean m_sortSelectFields;
6268

6369
/**
6470
* Describe helper
@@ -106,10 +112,24 @@ public abstract with sharing class fflib_SObjectSelector
106112
* @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well
107113
**/
108114
public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
115+
{
116+
this(includeFieldSetFields, enforceCRUD, enforceFLS, true);
117+
}
118+
119+
/**
120+
* Constructs the Selector
121+
*
122+
* @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well
123+
* @param enforceCRUD Enforce CRUD security
124+
* @param enforeFLS Enforce Field Level Security
125+
* @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance
126+
**/
127+
public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS, Boolean sortSelectFields)
109128
{
110129
m_includeFieldSetFields = includeFieldSetFields;
111130
m_enforceCRUD = enforceCRUD;
112131
m_enforceFLS = enforceFLS;
132+
m_sortSelectFields = sortSelectFields;
113133
}
114134

115135
/**
@@ -366,7 +386,8 @@ public abstract with sharing class fflib_SObjectSelector
366386
if(includeSelectorFields)
367387
{
368388
// select the Selector fields and Fieldsets and set order
369-
queryFactory.selectFields(new Set<SObjectField>(getSObjectFieldList()));
389+
queryFactory.selectFields(getSObjectFieldList());
390+
370391
List<Schema.FieldSet> fieldSetList = getSObjectFieldSetList();
371392
if(m_includeFieldSetFields && fieldSetList != null)
372393
for(Schema.FieldSet fieldSet : fieldSetList)
@@ -394,7 +415,9 @@ public abstract with sharing class fflib_SObjectSelector
394415
fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING;
395416
queryFactory.addOrdering(fieldNamePart, fieldSortOrder);
396417
}
397-
418+
419+
queryFactory.setSortSelectFields(m_sortSelectFields);
420+
398421
return queryFactory;
399422
}
400423
}

fflib/src/classes/fflib_SObjectSelectorTest.cls

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private with sharing class fflib_SObjectSelectorTest
144144
return; // Abort the test if unable to create a user with low enough acess
145145
System.runAs(testUser)
146146
{
147-
Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false);
147+
Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false, true);
148148
try
149149
{
150150
List<Account> result = (List<Account>) selector.selectSObjectsById(idSet);
@@ -205,16 +205,50 @@ private with sharing class fflib_SObjectSelectorTest
205205
}
206206
}
207207

208+
209+
@isTest
210+
static void testWithoutSorting()
211+
{
212+
//Given
213+
Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false, false);
214+
fflib_QueryFactory qf = selector.newQueryFactory();
215+
216+
Set<String> expectedSelectFields = new Set<String>{ 'Name', 'Id', 'AccountNumber', 'AnnualRevenue' };
217+
if (UserInfo.isMultiCurrencyOrganization())
218+
{
219+
expectedSelectFields.add('CurrencyIsoCode');
220+
}
221+
222+
//When
223+
String soql = qf.toSOQL();
224+
225+
//Then
226+
Pattern soqlPattern = Pattern.compile('SELECT (.*) FROM Account ORDER BY Name DESC NULLS FIRST , AnnualRevenue ASC NULLS FIRST ');
227+
Matcher soqlMatcher = soqlPattern.matcher(soql);
228+
soqlMatcher.matches();
229+
230+
List<String> actualSelectFields = soqlMatcher.group(1).deleteWhiteSpace().split(',');
231+
System.assertEquals(expectedSelectFields, new Set<String>(actualSelectFields));
232+
}
233+
234+
private static void assertEqualsSelectFields(String expectedSelectFields, String actualSelectFields)
235+
{
236+
Set<String> expected = new Set<String>(expectedSelectFields.deleteWhiteSpace().split(','));
237+
Set<String> actual = new Set<String>(actualSelectFields.deleteWhiteSpace().split(','));
238+
239+
System.assertEquals(expected, actual);
240+
}
241+
208242
private class Testfflib_SObjectSelector extends fflib_SObjectSelector
209243
{
210244
public Testfflib_SObjectSelector()
211245
{
212246
super();
213247
}
214248

215-
public Testfflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
249+
public Testfflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS, Boolean sortSelectFields)
216250
{
217-
super(includeFieldSetFields, enforceCRUD, enforceFLS);
251+
super(includeFieldSetFields, enforceCRUD, enforceFLS, sortSelectFields);
218252
}
219253

220254
public List<Schema.SObjectField> getSObjectFieldList()

0 commit comments

Comments
 (0)