Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lightweight Query Factory #108

Merged
merged 2 commits into from
Feb 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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