trailhead

Apply Selector Layer Principles in Apex

Learning Objectives

After completing this unit, you’ll be able to:

  • Create a Selector Apex class and make effective use of it.
  • Ensure that fields are consistently queried.
  • Implement sub-select and cross-object queries with the Selector pattern.
  • Dynamically query fields from a FieldSet in addition to your own.
  • Control when platform security enforcement is applied.

Implementing a Selector Class

We're going to wrap up this module with a deep dive into the Selector class and how to implement it. This Selector implementation uses the base class fflib_SObjectSelector to make building and executing SOQL queries easier, more consistent, and more compliant with less boilerplate code being written by the developer. It does this dynamically while still ensuring that compilation and reference integrity of the fields queried are maintained. It also provides useful common query functionality:

  • Inclusion of Organization feature - dependent fields, such as the CurrencyIsoCode field, that are only visible when the Multiple Currency feature is enabled.

  • Ability to include (optionally) fields defined by a FieldSet via the Administrator.

  • Enforcement of platform security by throwing an exception if the user does not have read access to the object. You can disable this feature via a constructor argument if the calling code wants to bypass it because the object is accessed indirectly on behalf of an operation that the user is performing.

The following shows the methods on the fflib_SObjectSelector base class, which is an abstract base class, meaning that you implement at least the methods marked as abstract before you can extend it. These methods are indicated with an A.

deploy button

Here is a basic example for the Product2 object. Although the selectSObjectsById method can be called directly from the base class, an overloaded version is implemented to clarify that it is returning a list of Product2 records.

public class ProductsSelector extends fflib_SObjectSelector {

    public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField> {
            Product2.Description,
            Product2.Id,
            Product2.IsActive,
            Product2.Name,
            Product2.ProductCode };
    }

    public Schema.SObjectType getSObjectType() {
        return Product2.sObjectType;
    }

    public List<Product2> selectById(Set<ID> idSet) {
        return (List<Product2>) selectSObjectsById(idSet);
    }
}

This example results in the following SOQL being generated and executed when the selectById method is called. You can also see some common base class behavior injected into the SOQL to provide consistent ordering. In this example, it defaults to the Name field because an alternative has not yet been specified.

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE id in :idSet
  ORDER BY Name ASC NULLS FIRST

Implementing the getSObjectFieldList method defined a list of fields for the base class to query in the selectSObjectsById method, ensuring the queries made result in sObjects with fields that are always consistently populated. Preventing potential issues with inconsistently populated records can make for more fragile code-execution paths.

Pro Tip: The trade-off here is Apex heap size versus how frequently fields are needed by the various callers of the Selector methods. The recommendation is to use it to include only a balance of fields that most of your logic will find most useful most of the time. Omit little used or large text field fields in favor of providing these via dedicated methods returning lists of custom Apex types, as described later this unit.

You can also override the getOrderBy method to ensure that all queries built or executed by the base class share the same ordering criteria, as shown in the example below.

public override String getOrderBy() {
    return 'IsActive DESC, ProductCode';
}

When the above method is overridden, the generated SOQL from selectById now looks like this:

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE id in :idSet
  ORDER BY IsActive DESC NULLS FIRST , ProductCode ASC NULLS FIRST

Implementing Custom Selector Methods

So far, we’ve implemented the abstract methods on the base class and seen how it affects the query made through calling the base class selectById method. Now we’ll add some Selector class methods that can perform different queries to vary the criteria, selected fields, and other aspects of the queries you need to make.

To implement a custom selector method and still honor the consistency of the fields and ordering expressed by the Selector, you can call the methods already implemented above. The following example is a basic Dynamic SOQL example showing how to do this using simple string formatting.

public List<Opportunity> selectRecentlyUpdated(Integer recordLimit) {
    String query = String.format(
    'select {0} from {1} ' +
    'where SystemModstamp = LAST_N_DAYS:30 ' +
    'order by {2} limit {3}',
    new List<String> {
        getFieldListString(),
        getSObjectName(),
        getOrderBy(),
        String.valueOf(recordLimit)
      }
    );
    return (List<Opportunity>) Database.query(query);
}

Query Factory Approach to Building SOQL Queries

The above approach to building the query using String.format works, but becomes harder to read and maintain as the queries get more complex.

The fflib_SObjectSelector base class also offers a more object-oriented way of building queries using a builder pattern approach provided by the fflib_QueryFactory class. The aim of this class is to make the dynamic creation of SOQL statements more robust and less error prone than traditional string concatenation approaches. Its method signatures follow the fluent design model.

You can create your own instance of fflib_QueryFactory and call its methods to indicate which object and fields you want to query. However, the Selector base class provides the helper method newQueryFactory to do this for you, leveraging the methods you have implemented above. You can then customize that instance, as shown below with criteria (where clause) before requesting the query to be built by the factory via the toSOQL method and then executed in the traditional way.

public List<Product2> selectRecentlyUpdated(Integer recordLimit) {   
    return (List<Product2>) Database.query(
        /**
          Query factory has been pre-initialised by calling
          getSObjectFieldList(), getOrderBy() for you.
        */
        newQueryFactory().
        /**
          Now focus on building the remainder of the
          query needed for this method.
        */
        setCondition('SystemModstamp = LAST_N_DAYS:30').
        setLimit(recordLimit).
        // Finally build the query to execute
        toSOQL());
}

When the custom selector method is called with a parameter of 10, the following SOQL is executed.

SELECT Description, Id, IsActive, Name, ProductCode
  FROM Product2
  WHERE SystemModstamp = LAST_N_DAYS:30
  ORDER BY IsActive DESC NULLS FIRST, ProductCode ASC NULLS FIRST
  LIMIT 10

Partial Field Selection and Cross-Object Queries

This next example passes a false parameter to the newQueryFactory method to instruct the base class to ignore the fields specified in getSObjectFieldList when creating the query factory instance. This allows your selector method code to add specific fields from the Opportunity object, in this case, the related Account and User objects, to form a cross-object query. This method resides in the OpportunitiesSelector class in this case.

The other difference in the following example is, rather than returning an Opportunity sObject with only specific fields populated and expecting the caller to know what was populated. The method constructs around each record a small Apex inner class OpportunityInfo to explicitly expose only the queried field values. This is a much safer, self-documenting, and tighter contract to the caller.

public List<OpportunityInfo> selectOpportunityInfo(Set<Id> idSet) {
    List<OpportunityInfo> opportunityInfos = new List<OpportunityInfo>();
    for(Opportunity opportunity : Database.query(
            newQueryFactory(false).
                selectField(Opportunity.Id).
                selectField(Opportunity.Amount).
                selectField(Opportunity.StageName).
                selectField('Account.Name').
                selectField('Account.AccountNumber').
                selectField('Account.Owner.Name').
                setCondition('id in :idSet').
                toSOQL()))
        opportunityInfos.add(new OpportunityInfo(opportunity));
    return opportunityInfos;
}

public class OpportunityInfo {       
    private Opportunity opportunity;
    public Id Id { get { return opportunity.Id; } }     
    public Decimal Amount { get { return opportunity.Amount; } }        
    public String Stage { get { return opportunity.StageName; } }       
    public String AccountName { get { return opportunity.Account.Name; } }      
    public String AccountNumber { get { return opportunity.Account.AccountNumber; } }       
    public String AccountOwner { get { return opportunity.Account.Owner.Name; } }
    private OpportunityInfo(Opportunity opportunity) { this.opportunity = opportunity; }         
}

Pro Tip: You might also want to consider this option when using AggregateDatabaseResult to return an Apex class more tailored to the information contained within these types of results. Avoid proliferating inner classes using this approach. The default field selection from the selector is really what you’d expect to use most of the time and thus returns actual sObjects. Also consider reusing these classes between methods.

The above code generates the following SOQL statement:

SELECT Id, StageName, Account.AccountNumber, Account.Name, Account.Owner.Name
  FROM Opportunity WHERE id in :idSet
  ORDER BY Name ASC NULLS FIRST

FieldSet Support

Another feature of the fflib_SObjectSelector base class is to include fields referenced by a given FieldSet. This makes the Selector semi-dynamic and allows you to use the results in conjunction with Visualforce pages that require the fields to have been queried. This example shows how this is done through a constructor parameter used to control the default inclusion of Fieldset fields. You must also override the getSObjectFieldSetList method. The rest of the Selector is the same.

public class ProductSelector extends fflib_SObjectSelector {

    public ProductsSelector() {
        super(false);
    }

    public ProductsSelector(Boolean includeFieldSetFields) {
        super(includeFieldSetFields);
    }


    public override List<Schema.FieldSet> getSObjectFieldSetList() {
        return new List<Schema.FieldSet>
                { SObjectType.Product2.FieldSets.MyFieldSet };
    }

    // Reminder of the Selector methods are the same
    // ...
}

Here is a short example using this new Fieldset constructor parameter. Although the sample code makes the assumption that the MyText__c field exists and has been added to the Fieldset, it’s only when the true parameter is passed to its constructor that the base class dynamically injects fields that the Administrator has added to the MyFieldSet.

// Test data
Product2 product = new Product2();
product.Description = 'Something cool';
product.Name = 'CoolItem';
product.IsActive = true;
product.MyText__c = 'My Text Field';
insert product;                 

// Query (including FieldSet fields)
List<Product2> products =
  new ProductsSelector(true).selectById(new Set<Id> { product.Id });


// Assert (FieldSet has been pre-configured to include MyText__c here)
System.assertEquals('Something cool', products[0].Description);    
System.assertEquals('CoolItem', products[0].Name);     
System.assertEquals(true, products[0].IsActive);       
System.assertEquals('My Text Field', products[0].MyText__c);

Using Specific FieldSets in Custom Selector Methods

You might have several FieldSets on your object and not want all Selector methods to utilize those expressed at the Selector class level. You can instead choose to allow them to be passed in as parameters to your selector methods and avoid registering, as shown in the example.

public List<Product2> selectById(Set<ID> idSet, Schema.FieldSet fieldSet) {
  return (List<Product2>) Database.query(
    newQueryFactory()
      .selectFieldSet(fieldSet)
      .setCondition('id in :idSet')
      .toSOQL()
  );
}

Advanced: Reusing Field Lists for Cross-Object and Sub-Select Queries

You can also create instances of Selector classes representing child objects when implementing custom Selector methods leveraging cross-object field and sub-select queries.

Using child Selectors in this process ensures that you’re also leveraging the Selector fields you’ve defined for these objects, even when those records are queried as part of a sub-select or cross-object query.

The example below leverages the addQueryFactorySubselect and the configureQueryFactoryFields base class methods. By creating instances of the child object Selectors, these methods inject the Selector fields into the query factory instance provided by the parent object custom Selector method that will eventually execute the query.

Here is an example that both queries Opportunities, child Opportunity Products, and related information from the Product and Pricebook objects reusing the respective child Selector classes.

public List<Opportunity> selectByIdWithProducts(Set<ID> idSet) {

    // Query Factory for this Selector (Opportunity)
    fflib_QueryFactory opportunitiesQueryFactory = newQueryFactory();

    // Add a query sub-select via the Query Factory for the Opportunity Products
    fflib_QueryFactory lineItemsQueryFactory =
        new OpportunityLineItemsSelector().
            addQueryFactorySubselect(opportunitiesQueryFactory);

    // Add cross object query fields for Pricebook Entry, Products and Pricebook
    new PricebookEntriesSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry');
    new ProductsSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Product2');
    new PricebooksSelector().
        configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Pricebook2');

    // Set the condition and build the query
    return (List<Opportunity>) Database.query(
        opportunitiesQueryFactory.setCondition('id in :idSet').toSOQL());
}

This results in the following SOQL query. Note that even the sub-select reuses the default order by as specified by the OpportunityLineItemsSelector (not shown in this unit’s examples).

SELECT
  AccountId, Amount, CloseDate, Description,
  DiscountType__c, ExpectedRevenue, Id, Name,
  Pricebook2Id, Probability, StageName, Type,  
  (SELECT
      Description, Id, ListPrice, OpportunityId,
      PricebookEntryId, Quantity, SortOrder,
      TotalPrice, UnitPrice, PricebookEntry.Id,
      PricebookEntry.IsActive, PricebookEntry.Name,
      PricebookEntry.Pricebook2Id, PricebookEntry.Product2Id,
      PricebookEntry.ProductCode, PricebookEntry.UnitPrice,
      PricebookEntry.UseStandardPrice,
      PricebookEntry.Pricebook2.Description,
      PricebookEntry.Pricebook2.Id,
      PricebookEntry.Pricebook2.IsActive,
      PricebookEntry.Pricebook2.IsStandard,
      PricebookEntry.Pricebook2.Name,
      PricebookEntry.Product2.Description,
      PricebookEntry.Product2.Id,
      PricebookEntry.Product2.IsActive,
      PricebookEntry.Product2.Name,
      PricebookEntry.Product2.ProductCode
     FROM OpportunityLineItems
     ORDER BY SortOrder ASC NULLS FIRST, PricebookEntry.Name ASC NULLS FIRST)
FROM Opportunity WHERE id in :idSet
ORDER BY Name ASC NULLS FIRST

Controlling Object and Field Level Security Enforcement

By default, the fflib_SObjectSelector base class enforces Salesforce object security by throwing an exception if the running user does not have read access to the object. However, it doesn’t enforce field-level security by default unless explicitly enabled, as described in this section.

As per similar logic in the Domain layer base class mentioned in the earlier unit, this enforcement applies for all types of access to the object, whether through a controller, service or domain. Internally, some service or domain logic might want to access an object on behalf of a user who doesn’t have the required permission.

However, if you prefer to disable this default base class security enforcement and implement this yourself, you can use configuration parameters on the base class constructor. The following example shows how to do this in each Selector class constructor. You can create your own base class and then extend that class for all Selector classes to avoid additional boilerplate code and standardize on your own application rules.

public PricebookEntriesSelector() {
    super(false, // Do not include FieldSet fields
          false, // Do not enforce Object level security
          false); // Do not enforce Field level security
}

If you prefer to let the caller drive enforcement, consider adding an overloaded constructor like the following one, thus letting the default constructor default to no security and require callers to utilize the other constructor to request that enforcement be enabled as needed.

public PricebookEntriesSelector(Boolean enforceObjectAndFieldSecurity) {
    super(false, // Do not include FieldSet fields
      enforceObjectAndFieldSecurity, enforceObjectAndFieldSecurity);
}

Controlling Sharing Rule Security Enforcement

Another form of security enforcement that Selector methods need to consider is whether sharing rules are applied. As per the design considerations for the Service layer or Visualforce or Lightning controller classes, the with sharing keyword is best practice. Any code in these classes that calls a Selector method also executes in this context. If these conventions are followed, the Selector code is running with sharing rules applied by default.

As with the filter criteria expressed in the where clause when executing queries, the sharing keyword also affects the selected recordset. To encapsulate a requirement to select all records regardless of sharing rules, you can follow a pattern of an explicit (method name) and internal elevation by using a private inner class annotated with the required without sharing keyword. This approach encapsulates this requirement and avoids the caller having to express this need by artificially creating a class to invoke this behavior.

public class OpportunitiesSelector extends fflib_SObjectSelector {
    public List<Opportunity> selectById(Set<Id> idSet) {
        // This method simply runs in the sharing context of the caller
        // ...
        return opportunities;
    }

    public List<OpportunityInfo> selectOpportunityInfoAsSystem(Set<Id> idSet) {
        // Explicitly run the query in a 'without sharing' context
        return new SelectOpportunityInfo().selectOpportunityInfo(this, idSet);
    }

    private without sharing class SelectOpportunityInfo {
        public List<OpportunitiesSelector.OpportunityInfo> selectOpportunityInfo(OpportunitiesSelector selector, Set<Id> idSet) {
            // Execute the query as normal
            // ...
           return opportunityInfos;             
        }
    }
}

Resources

retargeting