Skip to main content

Building Your Selector

New Selector Approach!

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:

  1. Small Selector Classes - Selector classes should be small and contain ONLY base query configurations (fields, sharing settings) and very generic methods (byId, byRecordType)
  2. Build SOQL Inline Where Needed - Business-specific SOQL queries should be built inline using the SOQL builder exactly where they're needed
  3. Eliminate Method Naming Overhead - Since queries are created inline, there's no need to spend time finding appropriate method names
  4. 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.

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)
SOQL_Account.cls
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;
}
}
ExampleController.cls
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.

SOQL_Contact.cls
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);
}
}
ExampleController.cls
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

SOQL_Account.cls
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);
}
}
ExampleController.cls
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.

BaseAccountSelector.cls
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));
}
}
MyTeam_AccountSelector.cls
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();
}
}
ExampleController.cls
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.

SOQL_Account.cls
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));
}
}
ExampleController.cls
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.

OpportunitySelector.cls
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);
}
}