Learn Selector Layer Principles
Learning Objectives
After completing this unit, you’ll be able to:
- Explain the benefits of using the Selector layer.
- Explain the features of the Selector layer.
- Understand how the Selector layer fits within your application architecture and the platform.
Follow Along with Trail Together
Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series.
(This clip starts at the 36:04 minute mark, in case you want to rewind and watch the beginning of the step again.)
Get to Know the Selector Layer
During this module a major focus has been ensuring good separation of concerns, making large complex enterprise-level code bases more clear, self-documenting, robust, and adaptable to change due to refactoring or functional evolution. Let's continue with this trend.
In this unit, you learn about the Selector Pattern, a layer of code that encapsulates logic responsible for querying information from standard objects and your custom objects. The selector layer feeds that data into your Domain layer and Service layer code. You can also reuse selector classes from other areas that require querying, such as Batch Apex and controllers.
The following illustration shows where the Selector class fits within the pattern class hierarchy. It is mostly used by classes within the Service layer (behind the service boundary), such as Domain and the Service classes themselves. However, UI Controllers and Batch Apex classes can also leverage them directly.
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.
-
Query inconsistencies: The same queries being made from different places for the same information or criteria (or subtle variants) can lead to inconsistencies in your application. Perhaps certain criteria need to be applied and get lost as code is copied and pasted around over time. Think of a query as a piece of logic. You know that it’s not a great idea to copy and paste pieces of logic around. It’s also not such a good idea with code.
-
Query data inconsistencies: When queried record data (essentially sObjects) starts to flow around your logic, up and down and side to side around the code in the org, calling methods receiving the records can become fragile. Perhaps a piece of code requests that an Account or Opportunity be passed to it, yet it cannot guarantee beforehand which fields have been queried and thus the resulting content, causing the dreaded runtime error
System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: X.Y.
Developers can end up repeating another query over the same record set simply to query the required fields. Or perhaps the code paths flow in such a way that Account records queried by different queries get passed to a shared function. Either way this is not good.
-
Security inconsistencies: Salesforce requires that all Apex code adheres to the object security of the running user. Apex can run at the system level also, making it the developer’s responsibility to check security ahead of making the query. Fortunately, it doesn’t take a lot of Apex code to do this, but it’s easily overlooked and not easy to test for in unit tests. Keeping this in a selector makes life much easier.
The Selector pattern that we are going to talk about helps with these issues.
Selector Pattern
The following is Martin Fowler’s definition of the Mapper pattern on which the Selector pattern is based.
Mapper pattern - "A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself." —Fowler, Martin
This pattern uses the term Selector rather than Mapper. Selector better reflects a key difference in the Salesforce implementation of the Mapper pattern, one which Stephen Willcock observed when developing the original Apex implementation of this pattern. The fflib_SObjectSelector
base class introduced in this unit is based on this.
The difference is that the Selector pattern is not mapping a database result set to a data object or even a Domain representation. Rather, in the Salesforce context, its primary role is to provide SObject records, the platform’s native way to express data queried from the database. In some cases, as you’ll see later in this unit, it can provide other Apex representations of data, but essentially it is merely selecting data in this instance, hence the basis for the name change of this pattern.
In terms of Separation of Concerns, a Selector concerns itself with providing the following:
-
Visibility, reusability, and maintainability - The selector makes it easy to find and maintain database query logic. When updating query criteria or reflecting schema changes, such as adding commonly used fields with more ease and less risk to the rest of the code base. The selector should use the compile time references to field names, even if dynamic queries are being constructed. Doing so ensures that when fields are deleted, the platform prevents the deletion if references exist in the code.
-
Predictability of queried data - It must be clear what the selector is doing in the method name and what it’s returning. Returning inconsistently populated fields in sObject records is not always a good model, because it’s not clear to the caller or to whomever the caller passes the resulting record data onto what was field-wise, resulting in runtime exceptions. The Selector pattern ensures that queried SObject records have a minimal set of fields queried. Developers can view this as part of the “selector’s contract to the rest of the org.”
-
Security - Provide a means for the caller to opt in or opt out (for system level scenarios) of security checks that enforce sharing and permissions applied to the current user context.
-
Platform sympathy - Make queries as optimal as possible, mainly expressing criteria via sets, thus encouraging the caller to be bulkified in its code when calling the selector methods. Selectors should also be able to balance the need for consistency of queried field data versus optimum field data when larger datasets are involved and the heap is a concern.
Design Considerations
When writing a Selector class, consider the following design aspects:
Using a Selector Class
How do we actually put these design considerations into practice? The following is a simple example used to initialize a Domain class instance with records. This selector uses non-static methods to provide its functionality, so an instance is created. If the caller retains the selector instance, there’s scope to perform some configuration and caching if needed.
List<Opportunity> opportunities = new OpportunitiesSelector().selectById(opportunityIds));
Here is some Service layer code (you might recognize it from the earlier unit) that is now using a selector to query and pass records to a Domain class, which is performing domain logic on the queried records. Thus, the inline SOQL logic has now been factored out of the Service and Domain logic.
List<Opportunity> opportunityRecords = new OpportunitiesSelector().selectByIdWithProducts(opportunityIds); Opportunities opportunities = new Opportunities(opportunityRecords); opportunities.applyDiscount(discountPercentage, uow);
This example shows a selector in a Batch Apex start method to drive the records being processed in the execute.
public Database.QueryLocator start(Database.BatchableContext bc) { return new InvoicesSelector().queryLocatorInvoicesToProcess(); }
Testing Selector Classes
Of course, your Selector class code is covered through your tests around the Domain or Service layer tests. However, if you’re interested in doing more pure unit testing, Selector classes are much the same as any other class: Create the test data, and call the methods and assert the results.
@IsTest private static void whenQueryOpportuntiesGetProducts() { // Given Set<Id> testRecordIds = setupTestOpportunities(); // When OpportunitiesSelector selector = new OpportunitiesSelector(); List<Opportunity> opportunities = selector.selectByIdWithProducts(testRecordIds); // Then System.assertEquals(10, opportunities.size()); System.assertEquals(5, opportunities[0].OpportunityLineItems.size()); }
If you have included QueryLocator methods in your Selector, you can call the iterator method to obtain its resulting records and thus assert for the expected data in your Selector test.
Database.QueryLocator queryLocator = new ProductsSelector().queryLocatorById(new Set<Id> {product.Id}); Database.QueryLocatorIterator productsIterator = queryLocator.iterator(); Product2 queriedProduct = (Product2) productsIterator.next();