Building Your Selector
The concept of selectors in SOQL Lib is different from FFLib Selectors!
Old Approach
The FFLib Selector concept assumes that all queries should be stored in the selector class.
Benefits:
- Avoids query duplication
- Provides one place to manage all queries
Issues:
- One-time queries (like aggregation or case-specific queries) get added to selectors
- Huge classes with numerous methods
- Queries become difficult to reuse
- Similar methods with small differences (limit, offset, etc.)
- Challenges with method naming conventions
- Frequent merge conflicts in team environments
New Approach
SOQL Lib takes a different approach based on real-world project analysis.
Core Assumption:
Most SOQL queries in a project are one-time queries executed for specific business cases.
Our Solution:
- Small Selector Classes - Selector classes should be small and contain ONLY base query configurations (fields, sharing settings) and very generic methods (
byId
,byRecordType
) - Build SOQL Inline Where Needed - Business-specific SOQL queries should be built inline using the SOQL builder exactly where they're needed
- Eliminate Method Naming Overhead - Since queries are created inline, there's no need to spend time finding appropriate method names
- Preserve Selector Strengths - Maintain default selector configurations (default fields, sharing settings) and keep generic methods
Check examples in our repository.
SOQL Lib is flexible, allowing you to adjust the solution according to your specific needs.
We don't enforce one approach over another; you can choose what works best for your team.
A - Inheritance - extends SOQL, implements Interface + static (Recommended)
The Most Flexible Approach:
- The selector constructor maintains default configurations such as default fields, sharing mode, and field-level security
- Only very generic methods are kept in the selector class, with each method returning an instance of the selector to enable method chaining
- Additional fields, complex conditions, ordering, limits, and other SOQL clauses are built exactly where they're needed (e.g., in controller methods)
public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
public static 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 byIndustry(String industry) {
whereAre(Filter.with(Account.Industry).equal(industry));
return this;
}
public SOQL_Account byParentId(Id parentId) {
whereAre(Filter.with(Account.ParentId).equal(parentId));
return this;
}
}
public with sharing class ExampleController {
@AuraEnabled
public static List<Account> getPartnerAccounts(String accountName) {
return SOQL_Account.query()
.byRecordType('Partner')
.whereAre(SOQL.Filter.name().contains(accountName))
.with(Account.BillingCity, Account.BillingCountry)
.toList();
}
@AuraEnabled
public static List<Account> getAccountsByRecordType(String recordType) {
return SOQL_Account.query()
.byIndustry('IT')
.byRecordType(recordType)
.with(Account.Industry, Account.AccountSource)
.toList();
}
}
B - Composition - implements Interface + static
This approach uses SOQL.Selector
interface with static
methods for query construction.
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 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 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
This approach is particularly useful when you have different teams or development streams that require 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 using your own custom approach and conventions.
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();
}
}
F - Traditional FFLib Approach
This approach follows the traditional FFLib selector pattern for comparison purposes.
public inherited sharing class OpportunitySelector extends SOQL {
public OpportunitySelector() {
super(Opportunity.SObjectType);
// default settings
with(Opportunity.Id, Opportunity.AccountId)
.systemMode()
.withoutSharing();
}
public List<Opportunity> byRecordType(String rt) {
return whereAre(Filter.recordType().equal(rt)).toList();
}
public List<Opportunity> byAccountId(Id accountId) {
return with(Opportunity.AccountId).whereAre(Filter.with(Opportunity.AccountId).equal(accountId)).toList();
}
public Integer toAmount(Id opportunityId) {
return (Integer) byId(opportunityId).toValueOf(Opportunity.Amount);
}
}