diff --git a/fflib/src/classes/fflib_QueryFactory.cls b/fflib/src/classes/fflib_QueryFactory.cls index 0ee244f0a49..87e2230a653 100644 --- a/fflib/src/classes/fflib_QueryFactory.cls +++ b/fflib/src/classes/fflib_QueryFactory.cls @@ -52,45 +52,45 @@ * There is a google doc providing additional guideance on the use of this class with field sets at * https://docs.google.com/a/financialforce.com/document/d/1I4cxN4xHT4UJj_3Oi0YBL_MJ5chm-KG8kMN1D1un8-g/edit?usp=sharing **/ -public class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller +public virtual class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller public enum SortOrder {ASCENDING, DESCENDING} /** * This property is read-only and may not be set after instantiation. * The {@link Schema.SObjectType} token of the SObject that will be used in the FROM clause of the resultant query. **/ - public Schema.SObjectType table {get; private set;} + public Schema.SObjectType table {get; protected set;} @testVisible - private Set fields; - private String conditionExpression; - private Integer limitCount; - private Integer offsetCount; - private List order; + protected Set fields; + protected String conditionExpression; + protected Integer limitCount; + protected Integer offsetCount; + protected List order; /** * Integrate checking for READ Field Level Security within the selectField(s) methods * This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling * one of the selectField or selectFieldset methods. **/ - private Boolean enforceFLS; - - private Boolean sortSelectFields = true; - + protected Boolean enforceFLS; + + protected Boolean sortSelectFields = true; + /** * The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to * a query, as long as it isn't a subselect query itself. You may have many subselects inside * a query, but they may only be 1 level deep (no subselect inside a subselect) * to add a subselect, call the subselectQuery method, passing in the ChildRelationship. **/ - private Schema.ChildRelationship relationship; - private Map subselectQueryMap; + protected Schema.ChildRelationship relationship; + protected Map subselectQueryMap; - private String getFieldPath(String fieldName){ + protected String getFieldPath(String fieldName){ if(!fieldName.contains('.')){ //single field Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase()); if(token == null) throw new InvalidFieldException(fieldName,this.table); - if (enforceFLS) - fflib_SecurityUtils.checkFieldIsReadable(this.table, token); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(this.table, token); return token.getDescribe().getName(); } @@ -102,7 +102,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr String field = i.next(); Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(lastSObjectType).getField(field.toLowerCase()); DescribeFieldResult tokenDescribe = token != null ? token.getDescribe() : null; - + if (token != null && enforceFLS) { fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token); } @@ -116,7 +116,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr if(token == null) throw new InvalidFieldException(field,lastSObjectType); else - throw new NonReferenceFieldException(lastSObjectType+'.'+field+' is not a lookup or master-detail field but is used in a cross-object query field.'); + throw new NonReferenceFieldException(lastSObjectType+'.'+field+' is not a lookup or master-detail field but is used in a cross-object query field.'); } } @@ -126,7 +126,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr @TestVisible private static String getFieldTokenPath(Schema.SObjectField field){ if(field == null){ - throw new InvalidFieldException('Invalid field: null'); + throw new InvalidFieldException('Invalid field: null'); } return field.getDescribe().getName(); } @@ -134,7 +134,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /** * fflib_QueryFactory instances will be considered equal if they produce the same SOQL query. * A faster comparison will first be attempted to check if they apply to the same table, and contain the same number of fields selected. - * This method will never return true if the provided object is not an instance of fflib_QueryFactory. + * This method will never return true if the provided object is not an instance of fflib_QueryFactory. * @param obj the object to check equality of. **/ public boolean equals(Object obj){ @@ -144,7 +144,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } /** - * Construct a new fflib_QueryFactory instance with no options other than the FROM caluse. + * Construct a new fflib_QueryFactory instance with no options other than the FROM caluse. * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. * @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}. **/ @@ -157,18 +157,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /** * Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship. - * This should be used when constructing a subquery query for addition to a parent query. + * This should be used when constructing a subquery query for addition to a parent query. * Objects created with this constructor cannot be added to another object using the subselectQuery method. * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. * @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}. **/ - private fflib_QueryFactory(Schema.ChildRelationship relationship){ - this(relationship.getChildSObject()); + protected fflib_QueryFactory(Schema.ChildRelationship relationship){ + this(relationship.getChildSObject()); this.relationship = relationship; } /** - * This method checks to see if the User has Read Access on {@link #table}. + * This method checks to see if the User has Read Access on {@link #table}. * Asserts true if User has access. **/ public fflib_QueryFactory assertIsAccessible(){ @@ -197,16 +197,16 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr this.sortSelectFields = doSort; return this; } - + /** * Selects a single field from the SObject specified in {@link #table}. * Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact. * @param fieldName the API name of the field to add to the query's SELECT clause. **/ - public fflib_QueryFactory selectField(String fieldName){ + public fflib_QueryFactory selectField(String fieldName){ fields.add( getFieldPath(fieldName) ); return this; - } + } /** * Selects a field, avoiding the possible ambiguitiy of String API names. * @see #selectField(String) @@ -216,7 +216,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr public fflib_QueryFactory selectField(Schema.SObjectField field){ if(field == null) throw new InvalidFieldException(null,this.table); - if (enforceFLS) + if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, field); fields.add( getFieldTokenPath(field) ); return this; @@ -228,7 +228,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr public fflib_QueryFactory selectFields(Set fieldNames){ for(String fieldName:fieldNames){ fields.add( getFieldPath(fieldName) ); - } + } return this; } /** @@ -248,9 +248,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr public fflib_QueryFactory selectFields(Set fields){ for(Schema.SObjectField token:fields){ if(token == null) - throw new InvalidFieldException(); - if (enforceFLS) - fflib_SecurityUtils.checkFieldIsReadable(table, token); + throw new InvalidFieldException(); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(table, token); this.fields.add( getFieldTokenPath(token) ); } return this; @@ -258,14 +258,14 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /** * Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times. * @param fieldNames the set of {@link Schema.SObjectField}s to select. - * @exception InvalidFieldException if the fields are null {@code fields}. + * @exception InvalidFieldException if the fields are null {@code fields}. **/ public fflib_QueryFactory selectFields(List fields){ for(Schema.SObjectField token:fields){ if(token == null) throw new InvalidFieldException(); - if (enforceFLS) - fflib_SecurityUtils.checkFieldIsReadable(table, token); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(table, token); this.fields.add( getFieldTokenPath(token) ); } return this; @@ -278,11 +278,11 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } /** * This is equivielent to iterating the fields in the field set and calling {@link #selectField(String)} on each. - * @param fieldSet Select all fields included in the field set. + * @param fieldSet Select all fields included in the field set. * @param allowCrossObject if false this method will throw an exception if any fields in the field set reference fields on a related record. - * @exception InvalidFieldSetException if the fieldset is invalid for table {@code fields}. + * @exception InvalidFieldSetException if the fieldset is invalid for table {@code fields}. **/ - public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet, Boolean allowCrossObject){ + public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet, Boolean allowCrossObject){ if(fieldSet.getSObjectType() != table) throw new InvalidFieldSetException('Field set "'+fieldSet.getName()+'" is not for SObject type "'+table+'"'); for(Schema.FieldSetMember field: fieldSet.getFields()){ @@ -356,7 +356,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /** * @returns the selected fields **/ - public Set getSelectedFields() { + public Set getSelectedFields() { return this.fields; } @@ -364,10 +364,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. * @deprecated Replaced by {@link #subselectQuery(String relationshipName)} and {@link #subselectQuery(ChildRelationship relationship)} - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param related The related object type **/ - public fflib_QueryFactory subselectQuery(SObjectType related){ + public fflib_QueryFactory subselectQuery(SObjectType related){ System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.subselectQuery(Schema.SObjectType) is deprecated and will be removed in a future release. Use fflib_QueryFactory.subselectQuery(String) or fflib_QueryFactory.subselectQuery(ChildRelationship) instead.'); return setSubselectQuery(getChildRelationship(related), false); } @@ -376,29 +376,29 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. * @deprecated Replaced by {@link #subselectQuery(String relationshipName, Boolean assertIsAccessible)} and {@link #subselectQuery(ChildRelationship relationship, Boolean assertIsAccessible)} - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param related The related object type * @param assertIsAccessible indicates whether to check if the user has access to the subquery object **/ public fflib_QueryFactory subselectQuery(SObjectType related, Boolean assertIsAccessible){ - System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.subselectQuery(Schema.SObjectType, Boolean) is deprecated and will be removed in a future release. Use fflib_QueryFactory.subselectQuery(String, Boolean) or fflib_QueryFactory.subselectQuery(ChildRelationship, Boolean) instead.'); + System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.subselectQuery(Schema.SObjectType, Boolean) is deprecated and will be removed in a future release. Use fflib_QueryFactory.subselectQuery(String, Boolean) or fflib_QueryFactory.subselectQuery(ChildRelationship, Boolean) instead.'); return setSubselectQuery(getChildRelationship(related), assertIsAccessible); } /** * Add a subquery query to this query. If a subquery for this relationshipName already exists, it will be returned. * If not, a new one will be created and returned. - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param relationshipName The relationshipName to be added as a subquery **/ - public fflib_QueryFactory subselectQuery(String relationshipName){ + public fflib_QueryFactory subselectQuery(String relationshipName){ return subselectQuery(relationshipName, false); } /** * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param relationshipName The relationshipName to be added as a subquery * @param assertIsAccessible indicates whether to check if the user has access to the subquery object **/ @@ -407,23 +407,23 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr if (relationship != null) { return setSubselectQuery(relationship, assertIsAccessible); } - throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery with relationshipName = '+relationshipName +'. Relationship does not exist for ' + table.getDescribe().getName()); + throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery with relationshipName = '+relationshipName +'. Relationship does not exist for ' + table.getDescribe().getName()); } /** * Add a subquery query to this query. If a subquery for this relationshipName already exists, it will be returned. * If not, a new one will be created and returned. - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param relationship The ChildRelationship to be added as a subquery **/ - public fflib_QueryFactory subselectQuery(ChildRelationship relationship){ + public fflib_QueryFactory subselectQuery(ChildRelationship relationship){ return subselectQuery(relationship, false); } /** * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param relationship The ChildRelationship to be added as a subquery * @param assertIsAccessible indicates whether to check if the user has access to the subquery object **/ @@ -434,22 +434,22 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /** * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. - * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship * @param relationship The ChildRelationship to be added as a subquery **/ - private fflib_QueryFactory setSubselectQuery(ChildRelationship relationship, Boolean assertIsAccessible){ + protected fflib_QueryFactory setSubselectQuery(ChildRelationship relationship, Boolean assertIsAccessible){ if (this.relationship != null){ throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. You may not add a subselect query to a subselect query.'); - } + } if (this.subselectQueryMap == null){ this.subselectQueryMap = new Map(); } if (this.subselectQueryMap.containsKey(relationship)){ return subselectQueryMap.get(relationship); } - + fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship); - + //The child queryFactory should be configured in the same way as the parent by default - can override after if required subSelectQuery.setSortSelectFields(sortSelectFields); @@ -466,7 +466,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr public List getSubselectQueries(){ if (subselectQueryMap != null) { return subselectQueryMap.values(); - } + } return null; } @@ -474,103 +474,103 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * Get the ChildRelationship from the Table for the object type passed in. * @param objType The object type of the child relationship to get **/ - private Schema.ChildRelationship getChildRelationship(sObjectType objType){ - for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){ - //occasionally on some standard objects (Like Contact child of Contact) do not have a relationship name. - //if there is no relationship name, we cannot query on it, so throw an exception. - if (childRow.getChildSObject() == objType && childRow.getRelationshipName() != null){ - return childRow; - } - } - throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. Invalid relationship for table '+table + ' and objtype='+objType); - } + protected Schema.ChildRelationship getChildRelationship(sObjectType objType){ + for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){ + //occasionally on some standard objects (Like Contact child of Contact) do not have a relationship name. + //if there is no relationship name, we cannot query on it, so throw an exception. + if (childRow.getChildSObject() == objType && childRow.getRelationshipName() != null){ + return childRow; + } + } + throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. Invalid relationship for table '+table + ' and objtype='+objType); + } /** * Get the ChildRelationship from the Table for the relationship name passed in. * @param relationshipName The name of the object's ChildRelationship on get **/ - private Schema.ChildRelationship getChildRelationship(String relationshipName){ - for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){ - if (childRow.getRelationshipName() == relationshipName){ - return childRow; - } - } - return null; - } + protected Schema.ChildRelationship getChildRelationship(String relationshipName){ + for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){ + if (childRow.getRelationshipName() == relationshipName){ + return childRow; + } + } + return null; + } /** - * Add a field to be sorted on. This may be a direct field or a field + * Add a field to be sorted on. This may be a direct field or a field * related through an object lookup or master-detail relationship. * Use the set to store unique field names, since we only want to sort * by the same field one time. The sort expressions are stored in a list * so that they are applied to the SOQL in the same order that they - * were added in. + * were added in. * @param fieldName The string value of the field to be sorted on * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) * @param nullsLast whether to sort null values last (NULLS LAST keyword included). - **/ - public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction, Boolean nullsLast){ + **/ + public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction, Boolean nullsLast){ order.add( - new Ordering(getFieldPath(fieldName), direction, nullsLast) - ); + new Ordering(getFieldPath(fieldName), direction, nullsLast) + ); return this; - } + } - /** - * Add a field to be sorted on. This may be a direct field or a field - * related through an object lookup or master-detail relationship. - * Use the set to store unique field names, since we only want to sort - * by the same field one time. The sort expressions are stored in a list - * so that they are applied to the SOQL in the same order that they - * were added in. - * @param field The SObjectfield to sort. This can only be a direct reference. - * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) - * @param nullsLast whether to sort null values last (NULLS LAST keyword included). - **/ - public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){ + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * @param field The SObjectfield to sort. This can only be a direct reference. + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + * @param nullsLast whether to sort null values last (NULLS LAST keyword included). + **/ + public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){ order.add( - new Ordering(getFieldTokenPath(field), direction, nullsLast) - ); + new Ordering(getFieldTokenPath(field), direction, nullsLast) + ); return this; - } + } - /** - * Add a field to be sorted on. This may be a direct field or a field - * related through an object lookup or master-detail relationship. - * Use the set to store unique field names, since we only want to sort - * by the same field one time. The sort expressions are stored in a list - * so that they are applied to the SOQL in the same order that they - * were added in. - * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" - * is required, use one of the overloaded addOrdering methods which include this parameter. - * @param fieldName The string value of the field to be sorted on - * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) - **/ - public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction){ + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" + * is required, use one of the overloaded addOrdering methods which include this parameter. + * @param fieldName The string value of the field to be sorted on + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + **/ + public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction){ order.add( - new Ordering(getFieldPath(fieldName), direction) - ); + new Ordering(getFieldPath(fieldName), direction) + ); return this; - } + } - /** - * Add a field to be sorted on. This may be a direct field or a field - * related through an object lookup or master-detail relationship. - * Use the set to store unique field names, since we only want to sort - * by the same field one time. The sort expressions are stored in a list - * so that they are applied to the SOQL in the same order that they - * were added in. - * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" - * is required, use one of the overloaded addOrdering methods which include this parameter. - * @param field The SObjectfield to sort. This can only be a direct reference. - * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) - **/ - public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){ + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" + * is required, use one of the overloaded addOrdering methods which include this parameter. + * @param field The SObjectfield to sort. This can only be a direct reference. + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + **/ + public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){ order.add( - new Ordering(getFieldTokenPath(field), direction) - ); + new Ordering(getFieldTokenPath(field), direction) + ); return this; - } + } /** * Remove existing ordering and set a field to be sorted on. This may be a direct field or a field @@ -633,12 +633,25 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr Ordering ordr = new Ordering(getFieldTokenPath(field), direction); return setOrdering(ordr); } + /** + * Virtual method that is called at the start of the toSOQL method to allow final state of the factory + * to be set by an extending class prior to the SOQL query string being constructed. With the above hooks in place, + * the developers would then extend the fflib_QueryFactory with their own class and add more advanced WHERE clause + * building methods + **/ + public virtual void onBeforeToSOQL(){ + + } /** * Convert the values provided to this instance into a full SOQL string for use with Database.query * Check to see if subqueries queries need to be added after the field list. **/ public String toSOQL(){ + + // ensuring that extension class had a chance to provide more advanced where clause + onBeforeToSOQL(); + String result = 'SELECT '; //if no fields have been added, just add the Id field so that the query or subquery will not just fail if (fields.size() == 0){ @@ -646,18 +659,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr result += 'Id'; }else { List fieldsToQuery = new List(fields); - + if(sortSelectFields){ - fieldsToQuery.sort(); - } - + fieldsToQuery.sort(); + } + result += String.join(fieldsToQuery,', '); } - + if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){ for (fflib_QueryFactory childRow : subselectQueryMap.values()){ result += ', (' + childRow.toSOQL() + ') '; - } + } } result += ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName()); if(conditionExpression != null) @@ -669,7 +682,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr result += o.toSOQL() +', '; result = result.substring(0,result.length()-2); } - + if(limitCount != null) result += ' LIMIT '+limitCount; @@ -683,13 +696,13 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * Create a "deep" clone of this object that can be safely mutated without affecting the cloned instance * @return a deep clone of this fflib_QueryFactory **/ - public fflib_QueryFactory deepClone(){ + public fflib_QueryFactory deepClone(){ fflib_QueryFactory clone = new fflib_QueryFactory(this.table) - .setLimit(this.limitCount) - .setOffset(this.offsetCount) - .setCondition(this.conditionExpression) - .setEnforceFLS(this.enforceFLS); + .setLimit(this.limitCount) + .setOffset(this.offsetCount) + .setCondition(this.conditionExpression) + .setEnforceFLS(this.enforceFLS); Map subqueries = this.subselectQueryMap; if(subqueries != null) { @@ -706,16 +719,16 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr return clone; } - - public class Ordering{ - private SortOrder direction; - private boolean nullsLast; - private String field; + + public virtual class Ordering{ + protected SortOrder direction; + protected boolean nullsLast; + protected String field; public Ordering(String sobjType, String fieldName, SortOrder direction){ this( - fflib_SObjectDescribe.getDescribe(sobjType).getField(fieldName), - direction + fflib_SObjectDescribe.getDescribe(sobjType).getField(fieldName), + direction ); } /** @@ -729,11 +742,11 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr this(fflib_QueryFactory.getFieldTokenPath(field), direction, nullsLast); } @testVisible - private Ordering(String field, SortOrder direction){ + protected Ordering(String field, SortOrder direction){ this(field, direction, false); } @testVisible - private Ordering(String field, SortOrder direction, Boolean nullsLast){ + protected Ordering(String field, SortOrder direction, Boolean nullsLast){ this.direction = direction; this.field = field; this.nullsLast = nullsLast; @@ -750,10 +763,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } - + public class InvalidFieldException extends Exception{ - private String fieldName; - private Schema.SObjectType objectType; + protected String fieldName; + protected Schema.SObjectType objectType; public InvalidFieldException(String fieldname, Schema.SObjectType objectType){ this.objectType = objectType; this.fieldName = fieldName; @@ -762,5 +775,5 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } public class InvalidFieldSetException extends Exception{} public class NonReferenceFieldException extends Exception{} - public class InvalidSubqueryRelationshipException extends Exception{} + public class InvalidSubqueryRelationshipException extends Exception{} } diff --git a/fflib/src/classes/fflib_SObjectSelector.cls b/fflib/src/classes/fflib_SObjectSelector.cls index 8ec7ace33fb..2e652b6c400 100644 --- a/fflib/src/classes/fflib_SObjectSelector.cls +++ b/fflib/src/classes/fflib_SObjectSelector.cls @@ -28,11 +28,11 @@ * Class providing common database query support for abstracting and encapsulating query logic **/ public abstract with sharing class fflib_SObjectSelector - implements fflib_ISObjectSelector + implements fflib_ISObjectSelector { /** * Indicates whether the sObject has the currency ISO code field for organisations which have multi-currency - * enabled. + * enabled. **/ private Boolean CURRENCY_ISO_CODE_ENABLED { get { @@ -41,52 +41,57 @@ public abstract with sharing class fflib_SObjectSelector } return CURRENCY_ISO_CODE_ENABLED; } - set; + set; } - + /** * Should this selector automatically include the FieldSet fields when building queries? **/ - private Boolean m_includeFieldSetFields; - + protected Boolean m_includeFieldSetFields; + /** * Enforce FLS Security **/ - private Boolean m_enforceFLS; + protected Boolean m_enforceFLS; /** * Enforce CRUD Security **/ - private Boolean m_enforceCRUD; - - /** - * Order by field - **/ - private String m_orderBy; + protected Boolean m_enforceCRUD; + + /** + * Order by field + **/ + private String m_orderBy; /** * Sort the query fields in the select statement (defaults to true, at the expense of performance). * Switch this off if you need more performant queries. **/ private Boolean m_sortSelectFields; - - /** - * Describe helper - **/ - private fflib_SObjectDescribe describeWrapper { - get { - if(describeWrapper == null) - describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); - return describeWrapper; - } - set; - } - + + /** + * Describe helper + **/ + private fflib_SObjectDescribe describeWrapper { + get { + if(describeWrapper == null) + describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); + return describeWrapper; + } + set; + } + /** + * static variables + **/ + private static String DEFAULT_SORT_FIELD = 'CreatedDate'; + private static String SF_ID_FIELD = 'Id'; + /** * Implement this method to inform the base class of the SObject (custom or standard) to be queried **/ abstract Schema.SObjectType getSObjectType(); - + /** * Implement this method to inform the base class of the common fields to be queried or listed by the base class methods **/ @@ -99,21 +104,21 @@ public abstract with sharing class fflib_SObjectSelector { this(false); } - + /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well **/ public fflib_SObjectSelector(Boolean includeFieldSetFields) { this(includeFieldSetFields, true, false); } - + /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well **/ public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS) { @@ -123,7 +128,7 @@ public abstract with sharing class fflib_SObjectSelector /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well * @param enforceCRUD Enforce CRUD security * @param enforeFLS Enforce Field Level Security * @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance @@ -143,44 +148,59 @@ public abstract with sharing class fflib_SObjectSelector { return null; } - + /** - * Override this method to control the default ordering of records returned by the base queries, - * defaults to the name field of the object or CreatedDate if there is none + * Override this method to control the default ordering of records returned by the base queries, + * defaults to the name field of the object if it is not encrypted or CreatedDate if there the object has createdDated or Id **/ public virtual String getOrderBy() { - if(m_orderBy == null) { - m_orderBy = 'CreatedDate'; - if(describeWrapper.getNameField() != null) { - m_orderBy = describeWrapper.getNameField().getDescribe().getName(); - } - } - return m_orderBy; + if (m_orderBy == null) + { + Schema.SObjectField nameField = describeWrapper.getNameField(); + if (nameField != null && !nameField.getDescribe().isEncrypted()) + { + m_orderBy = nameField.getDescribe().getName(); + } + else + { + m_orderBy = DEFAULT_SORT_FIELD; + try { + if (describeWrapper.getField(m_orderBy) == null) + { + m_orderBy = SF_ID_FIELD; + } + } + catch(fflib_QueryFactory.InvalidFieldException ex) { + m_orderBy = SF_ID_FIELD; + } + } + } + return m_orderBy; } - /** + /** * Returns True if this Selector instance has been instructed by the caller to include Field Set fields **/ - public Boolean isIncludeFieldSetFields() + public Boolean isIncludeFieldSetFields() { return m_includeFieldSetFields; } - + /** * Returns True if this Selector is enforcing FLS **/ public Boolean isEnforcingFLS() { - return m_enforceFLS; + return m_enforceFLS; } - + /** * Returns True if this Selector is enforcing CRUD Security **/ public Boolean isEnforcingCRUD() { - return m_enforceCRUD; + return m_enforceCRUD; } /** @@ -191,38 +211,38 @@ public abstract with sharing class fflib_SObjectSelector **/ public fflib_StringBuilder.CommaDelimitedListBuilder getFieldListBuilder() { - return - new fflib_StringBuilder.CommaDelimitedListBuilder( - new List(newQueryFactory().getSelectedFields())); + return + new fflib_StringBuilder.CommaDelimitedListBuilder( + new List(newQueryFactory().getSelectedFields())); } /** - * Use this method to override the default FieldListBuilder (created on demand via getFieldListBuilder) with a custom one, + * Use this method to override the default FieldListBuilder (created on demand via getFieldListBuilder) with a custom one, * warning, this will bypass anything getSObjectFieldList or getSObjectFieldSetList returns * * @depricated See newQueryFactory - **/ + **/ public void setFieldListBuilder(fflib_StringBuilder.FieldListBuilder fieldListBuilder) { - // TODO: Consider if given the known use cases for this (dynamic selector optomisation) if it's OK to leave this as a null operation + // TODO: Consider if given the known use cases for this (dynamic selector optomisation) if it's OK to leave this as a null operation } /** * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList * * @depricated See newQueryFactory - **/ + **/ public String getFieldListString() { return getFieldListBuilder().getStringValue(); } - + /** * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList * @param relation Will prefix fields with the given relation, e.g. MyLookupField__r * * @depricated See newQueryFactory - **/ + **/ public String getRelatedFieldListString(String relation) { return getFieldListBuilder().getStringValue(relation + '.'); @@ -235,10 +255,10 @@ public abstract with sharing class fflib_SObjectSelector { return describeWrapper.getDescribe().getName(); } - + /** - * Performs a SOQL query, - * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) + * Performs a SOQL query, + * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) * - From the SObject described by getSObjectType * - Where the Id's match those provided in the set * - Ordered by the fields returned via getOrderBy @@ -248,10 +268,10 @@ public abstract with sharing class fflib_SObjectSelector { return Database.query(buildQuerySObjectById()); } - + /** - * Performs a SOQL query, - * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) + * Performs a SOQL query, + * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) * - From the SObject described by getSObjectType * - Where the Id's match those provided in the set * - Ordered by the fields returned via getOrderBy @@ -261,21 +281,21 @@ public abstract with sharing class fflib_SObjectSelector { return Database.getQueryLocator(buildQuerySObjectById()); } - + /** * Throws an exception if the SObject indicated by getSObjectType is not accessible to the current user (read access) * - * @depricated If you utilise the newQueryFactory method this is automatically done for you (unless disabled by the selector) + * @depricated If you utilise the newQueryFactory method this is automatically done for you (unless disabled by the selector) **/ public void assertIsAccessible() { if(!getSObjectType().getDescribe().isAccessible()) - throw new fflib_SObjectDomain.DomainException( - 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); + throw new fflib_SObjectDomain.DomainException( + 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); } /** - * Public acccess for the getSObjectType during Mock registration + * Public acccess for the getSObjectType during Mock registration * (adding public to the existing method broken base class API backwards compatability) **/ public SObjectType getSObjectType2() @@ -284,27 +304,27 @@ public abstract with sharing class fflib_SObjectSelector } /** - * Public acccess for the getSObjectType during Mock registration + * Public acccess for the getSObjectType during Mock registration * (adding public to the existing method broken base class API backwards compatability) **/ public SObjectType sObjectType() { return getSObjectType(); } - + /** * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by **/ public fflib_QueryFactory newQueryFactory() - { + { return newQueryFactory(m_enforceCRUD, m_enforceFLS, true); } - + /** * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by **/ public fflib_QueryFactory newQueryFactory(Boolean includeSelectorFields) - { + { return newQueryFactory(m_enforceCRUD, m_enforceFLS, includeSelectorFields); } @@ -314,109 +334,109 @@ public abstract with sharing class fflib_SObjectSelector **/ public fflib_QueryFactory newQueryFactory(Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) { - // Construct QueryFactory around the given SObject + // Construct QueryFactory around the given SObject return configureQueryFactory( - new fflib_QueryFactory(getSObjectType2()), - assertCRUD, enforceFLS, includeSelectorFields); + new fflib_QueryFactory(getSObjectType2()), + assertCRUD, enforceFLS, includeSelectorFields); } - /** - * Adds the selectors fields to the given QueryFactory using the given relationship path as a prefix - * - * // TODO: This should be consistant (ideally) with configureQueryFactory below - **/ - public void configureQueryFactoryFields(fflib_QueryFactory queryFactory, String relationshipFieldPath) - { - // Add fields from selector prefixing the relationship path - for(SObjectField field : getSObjectFieldList()) - queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName()); + /** + * Adds the selectors fields to the given QueryFactory using the given relationship path as a prefix + * + * // TODO: This should be consistant (ideally) with configureQueryFactory below + **/ + public void configureQueryFactoryFields(fflib_QueryFactory queryFactory, String relationshipFieldPath) + { + // Add fields from selector prefixing the relationship path + for(SObjectField field : getSObjectFieldList()) + queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName()); // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) if(Userinfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED) - queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode'); - } - + queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode'); + } + /** * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory **/ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory) - { - return addQueryFactorySubselect(parentQueryFactory, true); + { + return addQueryFactorySubselect(parentQueryFactory, true); } - + /** * Adds a subselect QueryFactory based on this selector to the given QueryFactor **/ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, Boolean includeSelectorFields) - { - fflib_QueryFactory subSelectQueryFactory = - parentQueryFactory.subselectQuery(getSObjectType2()); - return configureQueryFactory( - subSelectQueryFactory, - m_enforceCRUD, - m_enforceFLS, - includeSelectorFields); + { + fflib_QueryFactory subSelectQueryFactory = + parentQueryFactory.subselectQuery(getSObjectType2()); + return configureQueryFactory( + subSelectQueryFactory, + m_enforceCRUD, + m_enforceFLS, + includeSelectorFields); + } + + /** + * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory + **/ + public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName) + { + return addQueryFactorySubselect(parentQueryFactory, relationshipName, TRUE); + } + + /** + * Adds a subselect QueryFactory based on this selector to the given QueryFactor + **/ + public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName, Boolean includeSelectorFields) + { + fflib_QueryFactory subSelectQueryFactory = parentQueryFactory.subselectQuery(relationshipName); + return configureQueryFactory(subSelectQueryFactory, m_enforceCRUD, m_enforceFLS, includeSelectorFields); } - - /** - * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory - **/ - public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName) - { - return addQueryFactorySubselect(parentQueryFactory, relationshipName, TRUE); - } - - /** - * Adds a subselect QueryFactory based on this selector to the given QueryFactor - **/ - public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName, Boolean includeSelectorFields) - { - fflib_QueryFactory subSelectQueryFactory = parentQueryFactory.subselectQuery(relationshipName); - return configureQueryFactory(subSelectQueryFactory, m_enforceCRUD, m_enforceFLS, includeSelectorFields); - } /** * Constructs the default SOQL query for this selector, see selectSObjectsById and queryLocatorById - **/ + **/ private String buildQuerySObjectById() - { + { return newQueryFactory().setCondition('id in :idSet').toSOQL(); } - - /** - * Configures a QueryFactory instance according to the configuration of this selector - **/ - private fflib_QueryFactory configureQueryFactory(fflib_QueryFactory queryFactory, Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) + + /** + * Configures a QueryFactory instance according to the configuration of this selector + **/ + protected fflib_QueryFactory configureQueryFactory(fflib_QueryFactory queryFactory, Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) { // CRUD and FLS security required? if (assertCRUD) { - try { - // Leverage QueryFactory for CRUD checking - queryFactory.assertIsAccessible(); - } catch (fflib_SecurityUtils.CrudException e) { - // Marshal exception into DomainException for backwards compatability - throw new fflib_SObjectDomain.DomainException( - 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); - } + try { + // Leverage QueryFactory for CRUD checking + queryFactory.assertIsAccessible(); + } catch (fflib_SecurityUtils.CrudException e) { + // Marshal exception into DomainException for backwards compatability + throw new fflib_SObjectDomain.DomainException( + 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); + } } queryFactory.setEnforceFLS(enforceFLS); - - // Configure the QueryFactory with the Selector fields? + + // Configure the QueryFactory with the Selector fields? if(includeSelectorFields) { - // select the Selector fields and Fieldsets and set order + // select the Selector fields and Fieldsets and set order queryFactory.selectFields(getSObjectFieldList()); List fieldSetList = getSObjectFieldSetList(); if(m_includeFieldSetFields && fieldSetList != null) for(Schema.FieldSet fieldSet : fieldSetList) queryFactory.selectFieldSet(fieldSet); - + // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) if(Userinfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED) queryFactory.selectField('CurrencyIsoCode'); } - + // Parse the getOrderBy() for(String orderBy : getOrderBy().split(',')) { @@ -431,10 +451,10 @@ public abstract with sharing class fflib_SObjectSelector else if(fieldSortOrderPart.equalsIgnoreCase('ASC')) fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; queryFactory.addOrdering(fieldNamePart, fieldSortOrder, orderBy.containsIgnoreCase('NULLS LAST')); - } - - queryFactory.setSortSelectFields(m_sortSelectFields); + } - return queryFactory; - } -} \ No newline at end of file + queryFactory.setSortSelectFields(m_sortSelectFields); + + return queryFactory; + } +}