Selector Layer - wimvelzeboer/fflib-apex-extensions GitHub Wiki
The purpose of the selector is to retrieve data from a source and return it. The source can in theory be anything, but is currently limited to SObjectTypes. Its planned to change this structure so that it can be things like:
-
Database; c, mdt, etc
-
Platform cache
-
Runtime memory
-
selector methods always accept arguments in bulk. The arguments are typically primitive variables in lists or sets.
-
The return type is either a list of the (S)Objects containing all fields defined returned by the method
getSObjectFieldList()
, or a subclass (data-transfer-object) -
Avoid lazy loading Selector classes.
Fluent configurations (e.g.ignoreCRUD()
) might be active and will be applied to all selector method calls. Use the staticnewInstance
method on the selector to create a new instance every time a selector method is called, unless you are certain that the scope is well defined and configurations can be re-used across your logic.
Interface
public interface IAccountsSelector extends fflib_ISObjectSelector
{
List<Account> selectById(Set<Id> idSet);
List<Account> selectByShippingCountry(Set<String> countryNames);
List<Account> selectByOpportunity(List<Opportunity> opportunities);
List<Account> selectByOpportunity(IOpportunity opportunities);
...
..
.
}
Implementation
public without sharing AccountsSelector
extends fflib_SObjectSelector
implements IAccountsSelector
{
public static IAccountsSelector newInstance()
{
return (IAccountsSelector)
((fflib_SObjectSelector) Application.Selector.newInstance(Schema.Account.SObjectType))
.setDataAccess(fflib_SObjectSelector.DataAccess.USER_MODE);
}
public static IAccountsSelector newElevatedInstance()
{
return (IAccountsSelector)
((fflib_SObjectSelector) Application.Selector.newInstance(Schema.Account.SObjectType))
.setDataAccess(fflib_SObjectSelector.DataAccess.SYSTEM_MODE);
}
public List<Schema.SObjectField> getSObjectFieldList()
{
return new List<Schema.SObjectField> {
Account.Id,
Account.Name,
Account.Description,
Account.ShippingCountry
};
}
public List<Account> selectById(Set<Id> idSet)
{
(List<Account>) newQueryFactory().setCondition('id in :idSet').toSOQL(););
}
public List<Account> selectByShippingCountry(Set<String> countryNames)
{
return
(List<Account>) Database.query(
newQueryFactory()
.setCondition('ShippingCountry IN :countrynames')
.toSOQL()
);
}
public List<Account> selectByOpportunity(List<Opportunity> opportunities)
{
return selectByOpportunity(Opportunities.newInstance(opportunities));
}
public List<Account> selectByOpportunity(IOpportunity opportunities)
{
Set<Id> accountIds = opportunities.getAccountIds();
return selectById(accountIds);
}
}
A new instance of the selector is created via the static newInstance()
method on the selector class.
List<Accounts> records = (List<Account>) Application.Selector.selectById(accountIds);
Selectors can also be called via the static method 'newInstance' on the main implementation of the selector.
List<Accounts> records = AccountsSelector.newInstance().selectById(accountIds);
Alternatively you can create a static property on the main domain implementation;
public with sharing class Accounts extends fflib_SObjects implements Accounts
{
...
public static IAccountsSelector Selector
{
get
{
return (IAccountsService) Application.Selector.newInstance(Account.SObjectType);
}
}
...
}
With this static property you can call the selector via:
List<Accounts> records = Accounts.Selector.selectById(accountIds);
For the name of the selector class use the plural version of your Objects name, appended with 'Selector'.
e.g AccountsSelector
selector name signature | Description | Example |
---|---|---|
select By DomainType querylocator By DomainType |
select based on the relationshipfield between two domains types Use the singular version of the ObjectType name. |
List<Account> selectByOpportunity() Database.QueryLocator queryLocatorByOpportunity() |
select By FieldName(Set<..> values) queryLocator By FieldName(Set<…> values |
query all the records with values from one particular field. |
List<Account> selectByShippingCountry(Set<String> countryNames); Database.QueryLocator queryLocatorByShippingCountry(Set<String> countryNames); |
select SubClass By FieldName |
query all the records with values from one particular field. Use a plural version of the field name. |
List<AddressData> selectAddressDataById(Set<Id> accountIds); |
All queries created by the query factory are sorted by the name field of the object or CreatedDate if there is none.
When a specific selector method requires additional ordering we can use the addOrdering
method to add it,
or use setOrdering
to overwrite the default ordering entirely.
public List<Account> selectByIdOrderedByAccountNumber(Set<Id> idSet)
{
return (List<Race__c>)
Database.query(
newQueryFactory()
.setCondition('Id IN :idSet')
.setOrdering(Account.AccountNumber, fflib_QueryFactory.ASCENDING)
.toSOQL()
);
}
We can override the getOrderBy
method in the selector class to change the default ordering.
Be careful with this since it will change the ordering for all the selector methods in the entire class!
public override String getOrderBy()
{
return 'MyField__c DESC';
}
List<Account> records = AccountsSelector.newElevatedInstance().selectById(accountIds);
// or with static properties on the domain:
List<Account> records = Accounts.ElevatedSelector.selectById(accountIds);
fflib_QueryFactory.Ordering largeAccountsFirst =
new fflib_QueryFactory.Ordering(
Schema.Account.NumberOfEmployees,
fflib_QueryFactory.SortOrder.DESCENDING);
List<Account> records =
AccountsSelector.newInstance()
.setOrdering(largeAccountsFirst)
.setLimit(100)
.setOffset(10)
.includeFieldSetFields()
.unsortedSelectFields()
.selectById(idSet);
The execution time of SOQL statements can very a lot. A reason for long execution time can be that queries are not indexed. Queries that retrieved data from multiple tables are always non-indexed and therefore very slow. It is usually faster to execute two indexed queries than one non-indexed, especially when those tables contain large amount of records.
Instead of using a relationship query like the following:
List<Race__c> records = RacesSelector.newInstance().selectByIdWithLocation(raceIds);
doing two queries will take more lines of code, but it will execute much faster
// Create a domain with the records, this will use the method RacesSelector.selectSObjectById
Races races = Races.newInstance(raceIds);
// Use the domain to retrieve the location Ids;
Set<Id> locationIds = races.getLocationIds();
// Create a new domain with the location data
// Again using the standard selectSObjectById method from the LocationsSelector class
Locations locations = Location.newInstance(locationIds);
// Create a mapping between the two tables
Map<Id, Location__c> locationByRaceId = locations.getRecordsByRaceId()
Anothing thing that requires noticing is that we only use the standard selectSObjectById
method from the selector classes. No additional custom methods are required here. So, we might need to write a bit more code when calling the selector, but the selector class itself needs much less methods.
We do not write Unit tests for selector classes, but we do test them via integration testing. Those tests should test an entire feature from front to end. It should have a DML transactions writing to the database and many assertions to make sure the feature works as expected.
@IsTest
private class MyAccountFeatureTest
{
@IsTest
static void itShouldTestNewClientAccountCreation()
{
// GIVEN
Integer numberOfAccounts = 10;
IAccounts accounts = AccountsFactory.generateClientAccounts(numberOfAccounts);
accounts
.setShippingcountry('Holland')
.setRating(AccountLabels.Rating.Warm)
....
...
..
.
// WHEN
System.Test.startTest();
insert accounts.getRecords();
System.Test.stopTest();
IAccounts result =
Accounts.newInstance(
AccountsSelector.newInstance().selectById(accounts.getRecordIds())
);
// THEN - the shipping country should be set to 'Holland'
System.assert(numberOfAccounts, result.selectByShippingCountry('Holland').size());
// THEN - the rating should be set to the default 'Warm'
System.assert(numberOfAccounts, result.selectByRating('Warm').size());
}
}
public interface IContactsSelector extends fflib_ISObjectSelector
{
List<Contact> selectByAccount(Set<Id> accountIds);
List<Contact> selectByStatus(Set<String> status);
List<Contact> selectByIdWithCases(Set<Id> raceIds);
List<Contact> selectByIdWithAccount(Set<Id> raceIds);
RaceSummaries selectSummariesByRaceId(Set<Id> raceIds);
}
public virtual without sharing class ContactsSelector
extends fflib_SObjectSelector
implements IContactsSelector
{
public static IContactsSelector newInstance()
{
return (IContactsSelector)
((fflib_SObjectSelector) Application.Selector.newInstance(Schema.Contact.SObjectType))
.setDataAccess(fflib_SObjectSelector.DataAccess.USER_MODE);
}
/**
* Runs the query in System Mode, disabling FLS, CRUD & sharing rules
*
* @return New instance of the selector
*/
public static IContactsSelector newElevatedInstance()
{
return (IContactsSelector)
((fflib_SObjectSelector) Application.Selector.newInstance(Schema.Contact.SObjectType))
.setDataAccess(fflib_SObjectSelector.DataAccess.SYSTEM_MODE);
}
public ContactsSelector()
{
super();
}
/**
* Holds a list of fields to be returned by all selector methods
*
* @return Returns list of default Contact fields
*/
public List<Schema.SObjectField> getSObjectFieldList()
{
return new List<Schema.SObjectField>
{
Contact.Id,
Contact.AccountId,
Contact.BirthDate,
Contact.DoNotCall,
Contact.Email,
Contact.HasOptedOutOfEmail,
Contact.FirstName,
Contact.LastName
};
}
public List<Schema.SObjectField> getSObjectPartnerFieldList()
{
return new List<Schema.SObjectField>
{
Contact.Id,
Contact.AccountId,
Contact.AssistantName,
Contact.AssistantPhone,
Contact.Email,
Contact.FirstName,
Contact.LastName,
Contact.ReportsToId,
Contact.Title
};
}
public Schema.SObjectType getSObjectType()
{
return Contact.SObjectType;
}
public virtual List<Contact> selectById(Set<Id> idSet)
{
return (List<Contact>) newQueryFactory().setCondition('id in :idSet').toSOQL();
}
/**
* Query Contact records with the given Account Ids
*
* @param accountIds The Account Ids to query
*
* @return Returns the records containing the given Account Id values
*
* @example
* List<Contact> records = ContactsSelector().newInstance()
* .selectByAccountId( accountIds );
*
* Generated the following query:
* ------------------------------
* SELECT AccountId, Birthdate, DoNotCall, Email, FirstName,
* HasOptedOutOfEmail, Id, LastName
* FROM Contact
* WHERE AccountId IN :accountIds
* ORDER BY Name ASC NULLS FIRST
*/
public virtual List<Contact> selectByAccountId(Set<Id> accountIds)
{
return (List<Contact>)
Database.query(
newQueryFactory()
.setCondition('AccountId IN :accountIds')
.toSOQL()
);
}
/**
* Query Contact records with the given LeadSource values
* and includes the Account.Name field
*
* @param leadSources The LeadSource values to query
*
* @return Returns the records containing the given LeadSource values including the Account Name field
*
* @example
*
* Set<String> leadSources = new Set<String>{ 'Email' }
* List<Contact> records = ContactsSelector().newInstance()
* .selectByLeadSource( leadSources );
*
* Generated the following query:
* ------------------------------
* SELECT Account.Name,
* AccountId, Birthdate, DoNotCall, Email, FirstName,
* HasOptedOutOfEmail, Id, LastName
* FROM Contact
* WHERE LeadSource IN :leadSources
* ORDER BY Name ASC NULLS FIRST
*/
public virtual List<Contact> selectByLeadSource(Set<String> leadSources)
{
return (List<Contact>)
Database.query(
newQueryFactory()
.selectField('Account.Name')
.setCondition('LeadSource IN :leadSources')
.toSOQL()
);
}
/**
* Query Contact records with the given Ids
* and includes all the default Account fields
*
* @param idSet Record ids to query
*
* @return Returns the records containing the Id which include all the default Account fields
*
* @example
* ContactsSelector.newInstance()
* .selectByIdWithAccount( contactIds );
*
* Generated the following query:
* ------------------------------
* SELECT Account.AccountNumber, Account.Id, Account.Name, Account.Rating, Account.ShippingCountry,
* AccountId, Birthdate, DoNotCall, Email, FirstName,
* HasOptedOutOfEmail, Id, LastName
* FROM Contact
* WHERE Id in :idSet
* ORDER BY Name ASC NULLS FIRST
*/
public virtual List<Contact> selectByIdWithAccount(Set<Id> idSet)
{
fflib_QueryFactory queryFactory = newQueryFactory();
((fflib_SObjectSelector) AccountsSelector.newInstance())
.configureQueryFactoryFields(
queryFactory,
Contact.AccountId.getDescribe().getRelationshipName());
return Database.query(queryFactory.setCondition('Id in :idSet').toSOQL());
}
/**
* Query Contact records with the given Ids
* and includes all the related Case records
*
* @param idSet Record ids to query
*
* @return Returns the records containing the Id with related Case records
*
* @example
* ContactsSelector.newInstance()
* .selectByIdWithCases( contactIds );
*
* Generated the following query:
* ------------------------------
* SELECT AccountId, Birthdate, DoNotCall, Email, FirstName, HasOptedOutOfEmail, Id, LastName,
* (SELECT CaseNumber, Id, Priority, Status, Subject FROM Cases ORDER BY CaseNumber ASC NULLS FIRST )
* FROM Contact
* WHERE Id in :idSet
* ORDER BY Name ASC NULLS FIRST
*/
public virtual List<Contact> selectByIdWithCases(Set<Id> idSet)
{
fflib_QueryFactory queryFactory = newQueryFactory();
((fflib_SObjectSelector) CasesSelector.newInstance())
.addQueryFactorySubselect(
queryFactory,
'Case'
);
return (List<Contact>) Database.query(
queryFactory.setCondition('Id in :idSet').toSOQL()
);
}
/**
* Query Contact records with the given Ids
* and includes not the standard fields but the partner fields
*
* @param idSet Record ids to query
*
* @return Returns the records containing the Id which contain the partner fields.
*
* @example
* ContactsSelector.newInstance()
* .selectPartnersById( contactIds );
*
* Generated the following query:
* ------------------------------
* SELECT AccountId, AssistantName, AssistantPhone, Email, FirstName, Id, LastName, ReportsToId, Title
* FROM Contact
* WHERE Id in :idSet AND RecordType.Name='Partner'
* ORDER BY Name ASC NULLS FIRST
*/
public virtual List<PartnerContact> selectPartnersById(Set<Id> idSet)
{
List<Contact> result = (List<Contact>) Database.query(
newQueryFactory(false)
.selectFields(getSObjectPartnerFieldList())
.setCondition('Id in :idSet AND RecordType.Name=\'Partner\'')
.toSOQL()
);
return ContactsFactory.generatePartnerContacts(result);
}
}