Overview
Assumptions
- Small Selector Classes - The selector class should be small and contains ONLY query base configuration (fields, sharing settings) and very generic methods (
byId
,byRecordType
). Why?- Huge classes are hard to manage.
- A lot of merge conflicts.
- Problems with methods naming.
- Build SOQL inline in a place of need - Business-specific SOQLs should be built inline via
SOQL
builder in a place of need.- Most of the queries on the project are case-specific and are not generic. There is no need to keep them in the Selector class.
- Build SOQL dynamically via builder - Developers should be able to adjust queries with specific fields, conditions, and other SOQL clauses.
- Do not spend time on selector methods naming - It can be difficult to find a proper name for a method that builds a query. The selector class contains methods like
selectByFieldAAndFieldBWithDescOrder
. It can be avoided by building SOQL inline in a place of need. - Control FLS and sharing settings - Selector should allow to control Field Level Security and sharing settings by simple methods like
.systemMode()
,.withSharing()
,.withoutSharing()
. - Auto binding - The selector should be able to bind variables dynamically without additional effort from the developer side.
- Mock results in Unit Tests - The selector should allow mocking data in unit tests.
Concepts
SOQL Library consist of:
SOQL Builder
SOQL Selector
SOQL Builder
SOQL Builder allows to build query dynamically and execute it.
// SELECT Id, Name, Industry FROM Account
List<Account> accounts = SOQL.of(Account.SObjectType)
.with(Account.Id, Account.Name, Account.Industry)
.toList();
SOQL Selector
A selector layer contains code responsible for querying records from the database. Although you can place SOQL queries in other layers, a few things can happen as the complexity of your code grows. ~ Salesforce
SOQL Lib provides the whole new concept for Selectors usage.
Old Approach
FFLIB Selector concept assumes that all queries should be stored in the Selector class.
- To avoid duplicates.
- One place to manage all queries.
Issues:
- One-time queries (like aggregation, case specific) added to Selector.
- Huge class with a lot of methods.
- Queries are difficult to reuse.
- Similar methods with small differences like limit, offset.
- Problem with naming methods.
- Merge conflicts.
New Approach
The SOQL Lib has a slightly different approach.
Assumption:
Most of the SOQLs on the project are one-time queries executed for specific business case.
Solution:
- Small Selector Classes - Selector class should be small and contains ONLY query base configuration (fields, sharing settings) and very generic methods (
byId
,byRecordType
) - Build SOQL inline in a place of need - Business-specific SOQLs should be built inline via the SOQL builder in the place of need.
- Do not spend time on selector methods naming - Queries are created inline, so there's no need to find a name.
- Keep Selector Strengths - Set default Selector configuration (default fields, sharing settings), keep generic methods.
Build Your Own Selector
SOQL-Lib is agile, so you can adjust the solution according to your needs. We don't force one approach over another, you can choose your own. Here are our propositions:
A - Inheritance - extends SOQL, implements Interface + static
public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
public SOQL_Account query() {
return new SOQL_Account();
}
private SOQL_Account() {
super(Account.SObjectType);
// default settings
with(Account.Id, Account.Name, Account.Type)
.systemMode()
.withoutSharing();
}
public SOQL_Account byRecordType(String rt) {
whereAre(Filter.recordType().equal(rt));
return this;
}
public SOQL_Account byIndustry(String industry) {
with(Account.Industry)
.whereAre(Filter.with(Account.Industry).equal(industry));
return this;
}
public SOQL_Account byParentId(Id parentId) {
with(Account.ParentId)
.whereAre(Filter.with(Account.ParentId).equal(parentId));
return this;
}
public String toIndustry(Id accountId) {
return (String) byId(accountId).toValueOf(Account.Industry);
}
}
public with sharing class ExampleController {
@AuraEnabled
public static List<Account> getPartnerAccounts(String accountName) {
return SOQL_Account.query()
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.FilterGroup
.add(SOQL.Filter.name().contains(accountName))
.add(SOQL.Filter.recordType().equal('Partner'))
)
.toList();
}
@AuraEnabled
public static List<Account> getAccountsByRecordType(String recordType) {
return SOQL_Account.query()
.byRecordType(recordType)
.byIndustry('IT')
.with(Account.Industry, Account.AccountSource)
.toList();
}
@AuraEnabled
public static String getAccountIndustry(Id accountId) {
return SOQL_Account.query().toIndustry(accountId);
}
}
B - Composition - implements Interface + static
Use SOQL.Selector
and create static
methods.
public inherited sharing class SOQL_Contact implements SOQL.Selector {
public static SOQL query() {
// default settings
return SOQL.of(Contact.SObjectType)
.with(Contact.Id, Contact.Name, Contact.AccountId)
.systemMode()
.withoutSharing();
}
public static SOQL byRecordType(String rt) {
return query()
.whereAre(SOQL.Filter.recordType().equal(rt));
}
public static SOQL byAccountId(Id accountId) {
return query()
.whereAre(SOQL.Filter.with(Contact.AccountId).equal(accountId));
}
public static String toName(Id contactId) {
return (String) query().byId(contactId).toValueOf(Contact.Name);
}
}
public with sharing class ExampleController {
@AuraEnabled
public static List<Contact> getContactsByRecordType(String recordType) {
return SOQL_Contact.byRecordType(recordType)
.with(Contact.Email, Contact.Title)
.toList();
}
@AuraEnabled
public static List<Contact> getContactsRelatedToAccount(Id accountId) {
return SOQL_Contact.byAccountId(accountId).toList();
}
@AuraEnabled
public static String getContactName(Id contactId) {
return SOQL_Contact.toName(contactId);
}
}
C - Inheritance - extends SOQL + non-static
public inherited sharing class SOQL_Account extends SOQL {
public SOQL_Account() {
super(Account.SObjectType);
// default settings
with(Account.Id, Account.Name, Account.Type)
.systemMode()
.withoutSharing();
}
public SOQL_Account byRecordType(String rt) {
whereAre(Filter.recordType().equal(rt));
return this;
}
public SOQL_Account byIndustry(String industry) {
with(Account.Industry)
.whereAre(Filter.with(Account.Industry).equal(industry));
return this;
}
public SOQL_Account byParentId(Id parentId) {
with(Account.ParentId)
.whereAre(Filter.with(Account.ParentId).equal(parentId));
return this;
}
public String toIndustry(Id accountId) {
return (String) byId(accountId).toValueOf(Account.Industry);
}
}
public with sharing class ExampleController {
@AuraEnabled
public static List<Account> getPartnerAccounts(String accountName) {
return new SOQL_Account()
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.FilterGroup
.add(SOQL.Filter.name().contains(accountName))
.add(SOQL.Filter.recordType().equal('Partner'))
)
.toList();
}
@AuraEnabled
public static List<Account> getAccountsByRecordType(String recordType) {
return new SOQL_Account()
.byRecordType(recordType)
.byIndustry('IT')
.with(Account.Industry, Account.AccountSource)
.toList();
}
@AuraEnabled
public static String getAccountIndustry(Id accountId) {
return new SOQL_Account().toIndustry(accountId);
}
}
D - Composition - implements Interface + non-static
Very useful when you have different teams/streams that need different query configurations.
public inherited sharing virtual class BaseAccountSelector implements SOQL.Selector {
public virtual SOQL query() {
return SOQL.of(Account.SObjectType)
.with(Account.Id, Account.Name);
}
public SOQL byRecordType(String rt) {
return query()
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.Filter.recordType().equal(rt));
}
}
public with sharing class MyTeam_AccountSelector extends BaseAccountSelector implements SOQL.Selector {
public override SOQL query() {
return SOQL.of(Account.SObjectType)
.with(Account.Id, Account.AccountNumber)
.systemMode()
.withoutSharing();
}
}
public with sharing class ExampleController {
public static List<Account> getAccounts(String accountName) {
return new MyTeam_AccountSelector().query()
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.Filter.name().contains(accountName))
.toList();
}
public static List<Account> getAccountsByRecordType(String recordType) {
return new MyTeam_AccountSelector().byRecordType(recordType)
.with(Account.ParentId)
.toList();
}
}
E - Custom
Create Selectors in your own way.
public inherited sharing class SOQL_Account {
public static SOQL query {
return SOQL.of(Account.SObjectType)
.with(Account.Id, Account.Name);
}
public static SOQL byRecordType(String rt) {
return query
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.Filter.recordType().equal(rt));
}
}
public with sharing class ExampleController {
public static List<Account> getAccounts(String accountName) {
return SOQL_Account.query
.with(Account.BillingCity, Account.BillingCountry)
.whereAre(SOQL.Filter.name().contains(accountName))
.toList();
}
public static List<Account> getAccountsByRecordType(String recordType) {
return SOQL_Account.byRecordType(recordType)
.with(Account.ParentId)
.toList();
}
}