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.

Introduction

During this module a major focus has been ensuring a 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 on with this trend.

This unit introduces the Selector, a layer of code that encapsulates logic responsible for querying information from your custom objects and feeding it 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.

Service class in hierarchy

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 copy 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 your code, 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. Unfortunately, Apex runs at the system level, 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 the above 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." Martin Fowler, EAA Patterns

This module uses the term Selector rather than Mapper. Selector better reflects a key difference in the Force.com implementation of the Mapper pattern, one which Stephen Willock 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 Force.com context, its primary role is to provide SObject objects, 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 SOC, 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 partially populated sObjects 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.

  • 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 lists, 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:

  • Naming - Group selector functionality by object type. If you follow the naming convention described in the Domain pattern unit, you can group selector code with domain code by using the plural name of the object, for example, ProductsSelector or OpportunitiesSelector. This is not a strict rule, but a best practice. For example, you might want to create a Selector class that encapsulates query functionality for a specific engine or core module that you’re building in your application.

  • Methods - Methods can be static or instance scope. The latter permits using a base class and common behavior inheritance (as shown below). The names of the methods (typically prefixed with select) ideally indicate the information and any related child information, such as selectById and selectByIdWithProductLines, for example.

  • Method parameters - Always take lists of any criteria that the query uses. As per the rules for service and domain class logic, bulkification is key, so make this part of the contract of a selector class as well.

  • Returning sObject lists - Mainly Selector methods should return sObject lists. Although Apex now supports deterministic maps and sets, neither can be used to reflect a sort order. The solution is to either provide two method variants, selectByIdMap and selectByIdList, or just have selectById and allow the caller to wrap the returned list in a map if needed. For example:

  Map<Id, Account> accountsById =
    new Map<Id, Account>( accountsSelector.selectByIds(accountIds) );
  • Returning QueryLocators - It might be tempting when implementing Batch Apex to implement your query inline in the start method, however this breaks the rules of encapsulation and reuse. Instead consider a selector method to return a QueryLocator. For example:
  accountSelector.selectByIdsWithQueryLocator(accountIds)
  • Returning custom logical record sets - In some cases, returning the physical representation of an Account, Opportunity or MyObject__c via an SObject instance isn’t the best way to reflect the fields that have been queried. In cases where you want to query only a few fields, for example, to avoid heap or viewstate issues, or you are selecting fields from various relationships or performing an aggregate query, consider using a custom Apex class (Java calls them POJOs) to implement a wrapper for the query result to expose the query result fields more explicitly as a more logical representation of the specific queried information.

  • Security - Avoid using the with sharing or without sharing keywords on Selector Apex classes to ensure that the calling context inherits this context. Typically, this is the Service class, which, as per its design guidelines, must specify with sharing. If a SOQL query is required to access records outside of the user’s visibility, such code must explicitly elevate the execution context to do so as briefly as possible. A good approach is to use a private Apex inner class with the without sharing modifier applied. The next unit provides a hands-on example of this.

Using a Selector Class

How do we actually put this 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 SOQL logic has now been factored out of the Service and Domain logic.

Opportunities opportunities = new Opportunities(
    new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));
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();

Resources

retargeting