Skip to content

Commit

Permalink
Merge pull request #108 from patronmanager/LightweightQueryField
Browse files Browse the repository at this point in the history
Lightweight Query Factory
  • Loading branch information
afawcett committed Feb 18, 2017
2 parents 1fd9b96 + 6165233 commit 4e5f8c2
Show file tree
Hide file tree
Showing 3 changed files with 471 additions and 89 deletions.
198 changes: 147 additions & 51 deletions fflib/src/classes/fflib_QueryFactory.cls
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
**/
public Schema.SObjectType table {get; private set;}
@testVisible
private Set<QueryField> fields;
private List<QueryField> fields;
private String conditionExpression;
private Integer limitCount;
private Integer offset;
Expand All @@ -71,9 +71,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
/* 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;
@TestVisible
private Boolean enforceFLS = false;
@TestVisible
private Boolean lightweight = false;

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

private QueryField getFieldToken(String fieldName){

// FLS will not be enforced, so we are going to take a lot of shortcuts in the name of performance
if (this.lightweight) {
return new LightweightQueryField(fieldName);
}

QueryField result;
if(!fieldName.contains('.')){ //single field
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
Expand Down Expand Up @@ -130,19 +137,31 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
return false;
return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL();
}

/**
* 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}.
**/
public fflib_QueryFactory(Schema.SObjectType table){
this.table = table;
fields = new Set<QueryField>();
order = new List<Ordering>();
enforceFLS = false;
this(table, false);
}

/**
* Construct a new fflib_QueryFactory instance, allowing you to use LightweightQueryFields
* to build the query. This offers significant performance improvement in query build time
* at the expense of FLS enforcement, and up-front field validation.
* @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
* @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query.
**/
public fflib_QueryFactory(Schema.SObjectType table, Boolean lightweight) {
this.table = table;
this.fields = new List<QueryField>();
this.order = new List<Ordering>();
this.lightweight = lightweight;
this.enforceFLS = false;
}

/**
* 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.
Expand All @@ -151,10 +170,22 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* @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());
this(relationship, false);
}

/**
* 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.
* 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}.
* @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query.
**/
private fflib_QueryFactory(Schema.ChildRelationship relationship, Boolean lightweight){
this(relationship.getChildSObject(), lightweight);
this.relationship = relationship;
}

/**
* This method checks to see if the User has Read Access on {@link #table}.
* Asserts true if User has access.
Expand All @@ -171,6 +202,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* @param enforce whether to enforce field level security (read)
**/
public fflib_QueryFactory setEnforceFLS(Boolean enforce){
if (this.lightweight && enforce) {
throw new InvalidOperationException('Calling setEnforceFLS(true) on a "lightweight" QueryFactory instance is not allowed.');
}
this.enforceFLS = enforce;
return this;
}
Expand All @@ -179,10 +213,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* Sets a flag to indicate that this query should have ordered
* query fields in the select statement (this at a small cost to performance).
* If you are processing large query sets, you should switch this off.
* @deprecated Fields are ALWAYS sorted within the generated SOQL, so this method now does nothing.
* @param whether or not select fields should be sorted in the soql statement.
**/
public fflib_QueryFactory setSortSelectFields(Boolean doSort){
this.sortSelectFields = doSort;
return this;
}

Expand All @@ -192,7 +226,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* @param fieldName the API name of the field to add to the query's SELECT clause.
**/
public fflib_QueryFactory selectField(String fieldName){
fields.add( getFieldToken(fieldName) );
this.fields.add( getFieldToken(fieldName) );
return this;
}
/**
Expand All @@ -206,31 +240,41 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
throw new InvalidFieldException(null,this.table);
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, field);
fields.add( new QueryField(field) );
this.fields.add(getQueryFieldFromToken(field));
return this;
}

/**
* Returns the appropriate QueryField implementation, based on the "lightweight" flag
* @param field the {@link Schema.SObjectField} for the QueryField
* @returns either a QueryField, or LightweightQueryField object for the specified SObjectField
**/
private QueryField getQueryFieldFromToken(Schema.SObjectField field) {
QueryField qf;
if (this.lightweight)
qf = new LightweightQueryField(field);
else
qf = new QueryField(field);
return qf;
}

/**
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
* @param fieldNames the Set of field API names to select.
**/
public fflib_QueryFactory selectFields(Set<String> fieldNames){
List<String> fieldList = new List<String>();
Set<QueryField> toAdd = new Set<QueryField>();
for(String fieldName:fieldNames){
toAdd.add( getFieldToken(fieldName) );
}
fields.addAll(toAdd);
this.fields.add( getFieldToken(fieldName) );
}
return this;
}
/**
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
* @param fieldNames the List of field API names to select.
**/
public fflib_QueryFactory selectFields(List<String> fieldNames){
Set<QueryField> toAdd = new Set<QueryField>();
for(String fieldName:fieldNames)
toAdd.add( getFieldToken(fieldName) );
fields.addAll(toAdd);
this.fields.add( getFieldToken(fieldName) );
return this;
}
/**
Expand All @@ -243,8 +287,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
if(token == null)
throw new InvalidFieldException();
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add( new QueryField(token) );
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add(getQueryFieldFromToken(token));
}
return this;
}
Expand All @@ -258,8 +302,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
if(token == null)
throw new InvalidFieldException();
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add( new QueryField(token) );
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add(getQueryFieldFromToken(token));
}
return this;
}
Expand Down Expand Up @@ -326,12 +370,13 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
}

/**
* @deprecated Replaced by {@link #getSelectedFieldsAsList()}
* @returns the selected fields
**/
public Set<QueryField> getSelectedFields() {
return this.fields;
return new Set<QueryField>(this.fields);
}

/**
* 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.
Expand Down Expand Up @@ -420,11 +465,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
return subselectQueryMap.get(relationship);
}

fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship);
fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship, this.lightweight);

//The child queryFactory should be configured in the same way as the parent by default - can override after if required
subSelectQuery.setSortSelectFields(sortSelectFields);

if(assertIsAccessible){
subSelectQuery.assertIsAccessible();
}
Expand Down Expand Up @@ -501,7 +543,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
**/
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){
order.add(
new Ordering(new QueryField(field), direction, nullsLast)
new Ordering(getQueryFieldFromToken(field), direction, nullsLast)
);
return this;
}
Expand Down Expand Up @@ -539,7 +581,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
**/
public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){
order.add(
new Ordering(new QueryField(field), direction)
new Ordering(getQueryFieldFromToken(field), direction)
);
return this;
}
Expand All @@ -554,15 +596,20 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
if (fields.size() == 0){
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
result += 'Id ';
}else if(sortSelectFields){
List<QueryField> fieldsToQuery = new List<QueryField>(fields);
fieldsToQuery.sort(); //delegates to QueryFilter's comparable implementation
for(QueryField field:fieldsToQuery){
result += field + ', ';
} else {
// This bit of code de-dupes the list of QueryFields. Since we've moved away from using a Set to back this collection
// (for performance reasons related to https://github.com/financialforcedev/fflib-apex-common/issues/79), we de-dupe
// by first sorting the List of QueryField objects (in order of the String representation of the field path),
// then making a pass through the list leaving dupes out of the "fieldsToQuery" collection.
fields.sort(); // Sorts based on QueryFields's "comparable" implementation
// Now that the QueryField list is sorted, we can de-dupe
QueryField previousQf = null;
for(QueryField field : fields){
if (!field.equals(previousQf)) {
result += field + ', ';
}
previousQf = field;
}
}else{
for (QueryField field : fields)
result += field + ', ';
}

if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){
Expand Down Expand Up @@ -592,7 +639,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
**/
public fflib_QueryFactory deepClone(){

fflib_QueryFactory clone = new fflib_QueryFactory(this.table)
fflib_QueryFactory clone = new fflib_QueryFactory(this.table, this.lightweight)
.setLimit(this.limitCount)
.setCondition(this.conditionExpression)
.setEnforceFLS(this.enforceFLS);
Expand Down Expand Up @@ -664,8 +711,54 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
}
}

public class LightweightQueryField extends QueryField implements Comparable {
String fieldName;

private LightweightQueryField() {}

@TestVisible
private LightweightQueryField(String fieldName) {
// Convert strings to lowercase so they sort in a case insensitive manner
this.fieldName = fieldName.toLowercase();
}

@TestVisible
private LightweightQueryField(Schema.SObjectField field) {
// Convert strings to lowercase so they sort in a case insensitive manner
this.fieldName = field.getDescribe().getLocalName().toLowercase();
}

public override String toString() { return this.fieldName; }

public override Integer hashCode() {
return (this.fieldName == null) ? 0 : this.fieldName.hashCode();
}

public override Boolean equals(Object obj) {
return ((obj != null)
&& (obj instanceof LightweightQueryField)
&& (this.fieldName == ((LightweightQueryField) obj).fieldName));
}

public override Integer compareTo(Object obj) {
if (obj == null || !(obj instanceof LightweightQueryField))
return 1;

if (this.fieldName == null) {
if (((LightweightQueryField) obj).fieldName == null)
// Both objects are non-null, but their fieldName is null
return 0;
else
// Our fieldName is null, but theirs isn't
return -1;
}

// Both objects have non-null fieldNames, so just return the result of String.compareTo
return this.fieldName.compareTo(((LightweightQueryField) obj).fieldName);
}
}

public class QueryField implements Comparable{
public virtual class QueryField implements Comparable{
List<Schema.SObjectField> fields;

/**
Expand All @@ -681,7 +774,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
public List<SObjectField> getFieldPath(){
return fields.clone();
}


private QueryField() {}

@testVisible
private QueryField(List<Schema.SObjectField> fields){
if(fields == null || fields.size() == 0)
Expand All @@ -694,7 +789,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
throw new InvalidFieldException('Invalid field: null');
fields = new List<Schema.SObjectField>{ field };
}
public override String toString(){
public virtual override String toString(){
String result = '';
Integer size = fields.size();
for (Integer i=0; i<size; i++)
Expand All @@ -712,10 +807,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
}
return result;
}
public integer hashCode(){
public virtual integer hashCode(){
return String.valueOf(this.fields).hashCode();
}
public boolean equals(Object obj){
public virtual boolean equals(Object obj){
//Easy checks first
if(obj == null || !(obj instanceof QueryField))
return false;
Expand Down Expand Up @@ -744,7 +839,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* - QueryFields with more joins give +1, while fewer joins give -1
* - For anything else, compare the toStrings of this and the supplied object.
**/
public Integer compareTo(Object o){
public virtual Integer compareTo(Object o){
if(o == null || !(o instanceof QueryField))
return -2; //We can't possibly do a sane comparison against an unknwon type, go athead and let it "win"

Expand Down Expand Up @@ -774,5 +869,6 @@ 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{}
public class InvalidOperationException extends Exception{}
}
Loading

0 comments on commit 4e5f8c2

Please sign in to comment.