Implementing design guidelines

The methods in the Selector classes encapsulate common SOQL queries made by the application, such as selectById, as well as more business-related methods, such as selectByTeam. This helps the developers who consume the Selector classes identify the correct methods to use for the business requirement and avoids replication of SOQL queries throughout the application.

Each method also has some standard characteristics, such as the SObject fields selected by the queries executed, regardless of the method called. The overall aim is to allow the caller to focus on the record data returned and not how it was read from the database.

Naming conventions

By now, you're starting to get the idea about naming conventions. The Selector classes and methods borrow guidelines from other layers with a few tweaks. Consider the following naming conventions when writing the Selector code:

  • Class names: in naming a Selector class, it typically follows the same convention as a Domain class, taking the plural name of the object it is associated with and appending the word Selector at the end; though you can group common cross object queries into a single module scoped class:
    • Some bad examples are RaceSOQLHelper and SOQLHelper
    • Some good examples are RacesSelector, DriversSelector and RacingAnalyticsSelector
  • Method names: the select prefix is a good way to express the shared purpose of the Selector methods, followed by a description of the primary criteria and/or relationships used. As with the Service and Domain methods, avoid repeating terms already used in the class name:
    • Some bad examples are getRecords, getDrivers, loadDrivers, selectDriversById, and selectRacesAndContestants.
    • Some good examples are selectById, selectByTeam, selectByIdWithContestants, and selectByIdWithContestantsAndDrivers.
  • Method signatures: Selector methods typically return a Map, List, or QueryLocator exposing the resulting SObjects, thus supporting the bulkification needs of the platform and other layers such as the Domain class constructors and Batch Apex. Method parameters reflect the parameterized aspects of the WHERE clause; again, these should also be bulkified where applicable to do so:
    • Some bad examples are:
      selectById(Id recordId)
      DriverSelector.select(Set<Id>teamIds)
      Database.QueryLocatorqueryForBatch(Set<Id> ids)
    • Some good examples are:
      selectById(Set<Id>raceIds)
      selectByTeam(Set<Id>teamIds)

Bulkification

As you can see, based on the method naming and signature convention examples, as with the Service and Domain layers, it's important to consider that in most cases the caller will want to honor the bulkifciation best practices. Make sure that the method return types and parameter types are list types—those such as Set, List, Map, QueryLocator, or Iterator can be used.

Record order consistency

Quite often, the order by clause is overlooked when writing SOQL queries, and this can lead to client/controller developers implementing this themselves. The default ordering of the platform is non-deterministic, though it sometimes appears to reflect the insert order, so the absence of an order by clause can often go unnoticed in testing. By using a Selector, you can apply a default ordering to all Selector methods.

Tip

Returning Map<Id, SObject> instead of List<SObject> might seem like a good way to help callers process the information. Maps in Apex have a predictable iterator order, however it is not clear from the documentation if this is the order in which items are added to the map. If you're concerned about this, keep in mind the result of using the order by clause in the Selector might be affected if a Map is used. Given the ease with which maps by SObject ID are created by passing the list into the Map constructor, it's often best to leave this option to wrap or not wrap the list returned up to the calling code.

Querying fields consistently

It is a good practice to avoid re-querying the same record(s) more than once within the same Apex execution if it can be avoided. Rather than repeating queries for the same record solely to query different fields, it becomes desirable to factor code such that it can pass the queried data as parameters from one method to another, for example from a Service method to a Domain method.

However, this practice can become problematic as when using SOQL, the developer must explicitly list the fields needed at the time the statement is executed, typically stating only the fields needed by the most immediate code path ahead.

Later, as the code evolves, the SObject data is passed between methods, particularly shared code. Runtime exceptions can occur if an attempt is made to read from a field that was not originally included in the initial SOQL. This result in the same code executing successfully in one scenario but failing in another, due to SOQL statements for the same data being inconsistently expressed.

For example, consider that the Contestants.awardChampionshipPoint Domain class method is updated to utilize the RaceTime__c field as well as the RacePosition__c field. This method is called from the ContestantService and RaceService classes. Ignoring the Selector concept for a moment, the following change to ContestantService will work just fine to meet this new requirement:

Contestants contestants = new Contestants([select Id, RacePosition__c, RaceTime__c 
       from Contestant__c order by RacePosition__c]);
contestants.awardChampionshipPoints(uow);

However, if left unchanged, the following code from the RaceService class will cause the same method to fail with an SObject row was retrieved via SOQL without querying the requested field: Contestant__c.RaceTime__c exception:

List<Contestant__c> contestants = new List<Contestant__c>();
for(Race__c race :  
     [select Id, 
        (Select RacePosition__c from Contestants__r) 
      from Race__c where Id in :ids])  
contestants.addAll(race.Contestants__r);
new Contestants(contestants).awardChampionshipPoints(uow);

Also note that the order by clause is missing from this example, but specified in the previous one thus, in this case the order of the records returned is non-deterministic. These last two examples illustrate that the records passed to the shared Domain class Contestants have the potential to vary in order by caller. This could give rise to unexpected behavior, if the Domain class logic is dependent on the order.

These are the two possible ways to fix these inconsistencies, though neither is ideal:

  • Update the awardChampionshipPoints method to have it perform its own SOQL query, and ensure that it always gets the information it needs regardless of what was initially passed into the Domain class constructor. While this is arguably more contained, it is wasteful in regards to SOQL queries and performance.
  • Search for SOQL statements across the application code and update the corresponding SOQL logic in each area (including relationship sub selects).

Of course, ensuring that both areas have adequate unit and integration testing also somewhat reduces the risk, but neither really helps increase the performance or encourage SOQL reuse. One of the benefits of the Selector pattern is to encapsulate a list of commonly used fields such that all the methods on the Selector return sObjects populated with a consistent set of fields which can be relied on throughout the application code, as long as the Selector is used to query the records.

Note

Querying more fields can generally be at odds with the desire to maintain a low heap and/or viewstate size when querying a large number of records in a single execution context, particularly if the Selector is returning queried data for display on a Visualforce page. In a later section of this chapter, we will see how it is possible to use an Apex data class to represent only the field data queried.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.101.81