Design
Single class
SOQL Lib is a single-class solution.
You don't need to think about dependencies; everything you need is stored in SOQL.cls. The SOQL.cls
only takes around 1500 lines of code.
Different clauses are encapsulated in small, inner classes. All crucial information is kept at the top of the class, so developers can use it even without reading the documentation.
public static SubQuery SubQuery {
get {
return new QSubQuery();
}
}
public static FilterGroup FilterGroup { // A group to nest more filters
get {
return new QFilterGroup();
}
}
public static Filter Filter {
get {
return new QFilter();
}
}
public static InnerJoin InnerJoin {
get {
return new QJoinQuery();
}
}
public interface Selector {
Queryable query();
}
public interface Queryable {
Queryable of(SObjectType ofObject);
Queryable of(String ofObject); // Dynamic SOQL
Queryable with(SObjectField field);
Queryable with(SObjectField field1, SObjectField field2);
Queryable with(SObjectField field1, SObjectField field2, SObjectField field3);
Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
Queryable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
Queryable with(List<SObjectField> fields); // For more than 5 fields
Queryable with(String fields); // Dynamic SOQL
Queryable with(SObjectField field, String alias); // Only aggregate expressions use field aliasing
Queryable with(String relationshipName, SObjectField field);
Queryable with(String relationshipName, SObjectField field1, SObjectField field2);
Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3);
Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
Queryable with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
Queryable with(String relationshipName, List<SObjectField> fields); // For more than 5 fields
Queryable with(SubQuery subQuery); // SOQL.SubQuery
Queryable count();
Queryable count(SObjectField field);
Queryable count(SObjectField field, String alias);
Queryable delegatedScope();
Queryable mineScope();
Queryable mineAndMyGroupsScope();
Queryable myTerritoryScope();
Queryable myTeamTerritoryScope();
Queryable teamScope();
Queryable whereAre(FilterGroup filterGroup); // SOQL.FilterGroup
Queryable whereAre(Filter filter); // SOQL.Filter
Queryable whereAre(String conditions); // Conditions to evaluate
Queryable groupBy(SObjectField field);
Queryable groupByRollup(SObjectField field);
Queryable orderBy(String field); // ASC, NULLS FIRST by default
Queryable orderBy(String field, String direction); // dynamic order by, NULLS FIRST by default
Queryable orderBy(SObjectField field); // ASC, NULLS FIRST by default
Queryable orderBy(String relationshipName, SObjectField field); // ASC, NULLS FIRST by default
Queryable sortDesc();
Queryable nullsLast();
Queryable setLimit(Integer amount);
Queryable offset(Integer startingRow);
Queryable forReference();
Queryable forView();
Queryable forUpdate();
Queryable allRows();
Queryable systemMode(); // USER_MODE by default
Queryable withSharing(); // Works only with .systemMode()
Queryable withoutSharing(); // Works only with .systemMode()
Queryable mockId(String id);
Queryable preview();
Queryable stripInaccessible();
Queryable stripInaccessible(AccessType accessType);
Queryable byId(SObject record);
Queryable byId(Id recordId);
Queryable byIds(Iterable<Id> recordIds); // List or Set
Queryable byIds(List<SObject> records);
String toString();
Object toValueOf(SObjectField fieldToExtract);
Set<String> toValuesOf(SObjectField fieldToExtract);
Integer toInteger(); // For COUNT query
SObject toObject();
List<SObject> toList();
List<AggregateResult> toAggregated();
Map<Id, SObject> toMap();
Database.QueryLocator toQueryLocator();
}
public interface SubQuery { // SOQL.SubQuery
SubQuery of(String ofObject);
SubQuery with(SObjectField field);
SubQuery with(SObjectField field1, SObjectField field2);
SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3);
SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
SubQuery with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
SubQuery with(List<SObjectField> fields); // For more than 5 fields
SubQuery with(String relationshipName, List<SObjectField> fields);
SubQuery with(SubQuery subQuery); // SOQL.SubQuery
SubQuery whereAre(FilterGroup filterGroup); // SOQL.FilterGroup
SubQuery whereAre(Filter filter); // SOQL.Filter
SubQuery orderBy(SObjectField field);
SubQuery orderBy(String relationshipName, SObjectField field);
SubQuery sortDesc();
SubQuery nullsLast();
SubQuery setLimit(Integer amount);
SubQuery offset(Integer startingRow);
SubQuery forReference();
SubQuery forView();
}
public interface FilterGroup { // SOQL.FilterGroup
FilterGroup add(FilterGroup filterGroup); // SOQL.FilterGroup
FilterGroup add(Filter filter); // SOQL.Filter
FilterGroup add(String dynamicCondition); // Pass condition as String
FilterGroup anyConditionMatching(); // All group filters will be join by OR
FilterGroup conditionLogic(String order);
Boolean hasValues();
}
public interface Filter { // SOQL.Filter
Filter id();
Filter recordType();
Filter name();
Filter with(SObjectField field);
Filter with(String field);
Filter with(String relationshipName, SObjectField field);
Filter isNull(); // = NULL
Filter isNotNull(); // != NULL
Filter isTrue(); // = TRUE
Filter isFalse(); // = FALSE
Filter equal(Object value); // = :value
Filter notEqual(Object value); // != :value
Filter lessThan(Object value); // < :value
Filter greaterThan(Object value); // > :value
Filter lessOrEqual(Object value); // <= :value
Filter greaterOrEqual(Object value); // >= :value
Filter containsSome(List<String> values); // LIKE :values
Filter contains(String value); // LIKE :'%' + value + '%'
Filter endsWith(String value); // LIKE :'%' + value
Filter startsWith(String value); // LIKE :value + '%'
Filter contains(String prefix, String value, String suffix); // custom LIKE
Filter isIn(Iterable<Object> iterable); // IN :inList or inSet
Filter isIn(List<Object> inList); // IN :inList
Filter isIn(InnerJoin joinQuery); // SOQL.InnerJoin
Filter notIn(Iterable<Object> iterable); // NOT IN :inList or inSet
Filter notIn(List<Object> inList); // NOT IN :inList
Filter notIn(InnerJoin joinQuery); // SOQL.InnerJoin
Filter includesAll(Iterable<String> values); // join with ;
Filter includesSome(Iterable<String> values); // join with ,
Filter excludesAll(Iterable<String> values); // join with ,
Filter excludesSome(Iterable<String> values); // join with ;
Filter removeWhenNull(); // Condition will be removed for value = null
Boolean hasValue();
}
public interface InnerJoin { // SOQL.InnerJoin
InnerJoin of(SObjectType ofObject);
InnerJoin with(SObjectField field);
InnerJoin whereAre(FilterGroup filterGroup); // SOQL.FilterGroup
InnerJoin whereAre(Filter filter); // SOQL.Filter
}
@TestVisible
private static void setMock(String mockId, SObject record) {
setMock(mockId, new List<SObject>{ record });
}
@TestVisible
private static void setMock(String mockId, List<SObject> records) {
mock.setMock(mockId, records);
}
@TestVisible
private static void setCountMock(String mockId, Integer amount) {
mock.setCountMock(mockId, amount);
}
Functional Programming
SOQL Lib uses the concept called Apex Functional Programming.
You can see an example of it with SOQL.SubQuery
, SOQL.FilterGroup
, SOQL.Filter
and SOQL.InnerJoin
.
Those classes encapsulate the logic, and only necessary methods are exposed via interfaces.
Queryable whereAre(FilterGroup filterGroup); // SOQL.FilterGroup
Queryable whereAre(Filter filter); // SOQL.Filter
SOQL.of(Account.SObjectType)
.with(Account.Id, Account.Name);
.whereAre(SOQL.FilterGroup
.add(SOQL.Filter.id().equal(accountId))
.add(SOQL.Filter.with(Account.Name).contains(accountName))
.anyConditionMatching() // OR
)
.toList();
Composition over inheritance
SOQL Lib's Selector follows the rule of Composition over inheritance.
Instead of using inheritance
:
public with sharing class OpportunitiesSelector extends SObjectSelector {
// ...
}
We use composition
:
public inherited sharing class SOQL_Account implements SOQL.Selector {
public static SOQL query() {
return SOQL.of(Account.SObjectType)
.with(Account.Name, Account.AccountNumber)
.systemMode()
.withoutSharing();
}
}
This approach gives us a lot of flexibility. We can set not only default fields, FLS, and sharing rules but also add default conditions that will be used in every query.
Return SOQL instance
public inherited sharing class AccountSelector implements SOQL.Selector {
public static SOQL query() {
return SOQL.of(Account.SObjectType)
.with(Account.Name, Account.AccountNumber)
.systemMode();
}
public static SOQL byRecordType(String rt) {
return query()
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.Filter.recordType().equal(rt));
}
}
As you can see, the method byRecordType(String rt)
returns an instance of SOQL
instead of List<SObject>
or Object
. Why is that? You can adjust the query to your needs and add more SOQL clauses without duplicating the code.
AccountSelector.byRecordType(recordType).with(Account.ParentId).toList(); // additional field
AccountSelector.byRecordType(recordType).orderBy(Account.Name).toList(); // additionl order by name