Implementing custom query logic

If we take a look at the implementation of the selectSObjectById base class method we have been using so far in this chapter, the buildQuerySObjectById method code shown as following, gives an indication of how we implement custom Selector methods; it also highlights the newQueryFactory base class method usage:

public List<SObject> selectSObjectsById(Set<Id>idSet) {
  return Database.query(buildQuerySObjectById());
}
private String buildQuerySObjectById() {
  return newQueryFactory().
           setCondition('id in :idSet').
           toSOQL();
}

The newQueryFactory method exposes an alternative object orientated way to express a SOQL query. It follows the fluent design model with its methods making the configuration less verbose. For more information on this approach see https://en.wikipedia.org/wiki/Fluent_interface.

The instance of fflib_QueryFactory returned by this method is preconfigured with the object, fields, order by and any field set fields expressed through the selector methods discussed previously. As described earlier it will also enforce security enabled. So in general all you need to do is provide an SOQL where clause via the setCondition method, as shown in the last code. Finally, the toSOQL method returns the actual SOQL query string that is passed to the standard Database.query method.

Note

The fflib_QueryFactory class methods provide an alternative to using the standard string concatenation or String.format approaches when building dynamic SOQL queries. The original author of the fflib_QueryFactory class, Chris Peterson, was motivated in creating it to avoid the fragility that string-based methods can give as well as to provide a more readable means of understanding what such code was doing. In addition because the class supports using SObjectField references, as opposed to field name references in strings, such code is less prone to SOQL-injection vulnerabilities.

A basic custom Selector method

Let's look first at a basic example, which queries Driver__c based on the team they are assigned to. As per the guidelines, the parameters are bulkified and allow you to query for drivers across multiple teams if needed.

public List<Driver__c>selectByTeam(Set<Id>teamIds) {
  return (List<Driver__c>) 
    Database.query(
      newQueryFactory().
        setCondition('Team__c in :teamIds').
        toSOQL());
}

By using the newQueryFactory method, the fflib_QueryFactory instance is already populated with knowledge of the field list, object name, and order by clause specified by the selector. Thus this Selector method behaves consistently in terms of the fields populated and the order of the records returned.

A custom Selector method with subselect

In the following example, the querying being made uses a sub-select to also include child records (in this case, Contestants within Race). The key thing here is that the RacesSelector method (shown in the following code) reuses an instance of the ContestantsSelector class to access its field list and perform security checking:

public List<Race__c>selectByIdWithContestants(Set<Id>raceIds) {

  fflib_QueryFactoryracesQueryFactory = newQueryFactory();

  fflib_QueryFactorycontestantsSubQueryFactory = new ContestantsSelector().addQueryFactorySubselect(racesQueryFactory);

  return (List<Race__c>) Database.query(
    racesQueryFactory.setCondition('Id in :raceIds').toSOQL());
}

The preceding code uses the fflib_SObjectSelector method addQueryFactorySubselect. This method creates a new fflib_QueryFactory instance and passes it on to the query factory passed into the method, in this case, the racesQueryFactory. When the toSOQL method is called on the outer query factory it automatically generates sub-queries for any contained query factories.

By reusing an instance of the ContestantsSelector, it is ensured that no matter how a list of Contestant__c records are queried (directly or as part of a sub-select query), the fields are populated consistently. The getOrderBy method on ContestantSelector is also used to ensure that even though the records are being returned as part of a sub-select, the default ordering of Contestant records is also maintained.

Let's take another look at the example used earlier to illustrate how this pattern has avoided the dangers of inconsistent querying. Although the Contestant__c records are queried from two different selectors, RacesSelector and ContestantsSelector, the result in terms of fields populated is the same due to the reuse of the ContestantsSelector class in the preceding implementation.

The following example shows the standard selectById method being used to query Contestants to pass to the Contestants Domain class constructor, as part of the Service layer implementation to award championship points:

List<Contestant__c> contestants = 
   new ContestantsSelector().selectById(contestantIds);
new Contestants(contestants).awardChampionshipPoints(uow);

The following alternative example shows the custom selectByIdWithContestants method on RaceSelector being used to query Races and Contestants to pass to the Contestants Domain class constructor to achieve the same result:

List<Contestant__c> contestants = new List<Contestant__c>();
for(Race__c race : 
  new RacesSelector().selectByIdWithContestants(raceIds)) {
    contestants.addAll(race.Contestants__r);
}
new Contestants(contestants).awardChampionshipPoints(uow);

In either of these two examples, the fields and ordering of the records are consistent, despite the way in which they have been queried, either as a direct query or sub-select. This gives the developer more confidence in using the most optimum approach to query the records needed at the time.

The selectByIdWithContestants method generates the following SOQL:

select
  Name, TotalDNFs__c, Status__c, Season__c, Id,PollPositionLapTime__c, FastestLapBy__c, (select 
    Qualification1LapTime__c, ChampionshipPoints__c,Driver__c, GridPosition__c, Qualification3LapTime__c, 
    RacePosition__c, Name, DNF__c, RaceTime__c, 
    Id, DriverRace__c, Qualification2LapTime__c, Race__c
    from Contestants__r
    order by Race__r.Season__r.Name, Race__r.Name,
      RacePosition__c)
    from Race__c where id in :raceIds order by Name

A custom Selector method with related fields

In addition to querying child records, related records can also be queried using the SOQL field dot notation. This provides an additional way to optimize the querying of additional record information, along with having to make an additional query.

The following is an example of a custom Selector method for the ContestantsSelector class, where Contestant records are queried along with the related Driver record fields. The resulting Contestant__c objects expose the Driver__r field, which provides an instance of Driver__c populated with the Driver__c fields specified in DriversSelector:

public List<Contestant__c>selectByIdWithDriver(Set<Id>driverIds) {
  fflib_QueryFactorycontestantFactory = newQueryFactory();

  new DriversSelector().
    configureQueryFactoryFields(
    contestantFactory, 
    Contestant__c.Driver__c.getDescribe().getRelationshipName());

  return Database.query(
    contestantFactory.setCondition('Id in :driverIds').toSOQL());
}

Note

The fflib_SObjectSelector.configureQueryFactoryFields method adds the selector fields to the query factory passed as the first parameter, utilizing the relationship prefix specified in the second parameter.

Due to the reuse of DriversSelector in the ContestantsSelector classes method in the last code, the following examples return consistently populated Driver__c records. This is important to the logic contained within the Domain class's Drivers.verifyCompliance method. This flexibility allows for the most optimum way of querying Driver__c records and safer reuse of the Domain class method regardless of how the records were queried.

This first example constructs the Drivers domain class with results from the standard selectById method on the DriversSelector class:

List<Driver__c> drivers =   new DriversSelector().selectById(driverIds);
List<ComplianceService.VerifyResult> results =  
  new Drivers(drivers).verifyCompliance();

This second example achieves the same, but queries the Drivers records via the custom Selector method, selectByIdWithDriver, on the ContestantsSelector class:

List<Driver__c> drivers = new List<Driver__c>();
List<Contestant__c> contestants =   new ContestantsSelector().selectByIdWithDriver(contIds);
for(Contestant__c contestant : contestants)
drivers.add(contestant.Driver__r);
List<ComplianceService.VerifyResult> results = 
  new Drivers(drivers).verifyCompliance();

Again, both of these examples have shown that even when using related fields in SOQL, the resulting records can be kept in alignment with the desired fields as expressed by the selectors. The selectByIdWithDriver method generates the following SOQL:

SELECT       
  Qualification1LapTime__c, ChampionshipPoints__c, Driver__c,
  GridPosition__c, Qualification3LapTime__c, RacePosition__c,
  Name, DNF__c, RaceTime__c, Id, DriverRace__c,
  Qualification2LapTime__c, Race__c,
  Driver__r.FIASuperLicense__c, Driver__r.Name,
  Driver__r.Id, Driver__r.Team__c
 FROM Contestant__c
 WHERE Id in :driverIds
 ORDER BY Name

A custom Selector method with a custom data set

SOQL has a very rich dot notation to traverse relationship (lookup) fields to access other fields on related records within a single query. To obtain the following information in a tabular form, it needs to traverse up and down the relationships across many of the objects in the FormulaForce application object schema:

  • The season name
  • The race name
  • The race position
  • The driver's name
  • The car name

First, let's consider the following SOQL and then look at how it and the data it returns can be encapsulated in a ContestantsSelector class method. Note that the common order by clause is also applied here as was the case in the previous example:

select
  Race__r.Season__r.Name, 
  Race__r.Name,RacePosition__c,
  Driver__r.Name,
  Driver__r.Team__r.Name,
  Car__r.Name
from Contestant__c
where Race__c in :raceIds
order by 
  Race__r.Season__r.Name, Race__r.Name, RacePosition__c 

This query will result in partially populated Contestant__c objects, not only that, but relationship fields will also expose partially populated Season__c, Car__c, and Driver__c records. Thus, these are highly specialized instances of records to simply return into the calling code path.

The following code shows an alternative to the preceding query, using a new ContestantsSelector method. The biggest difference from the previous ones is that it returns a custom Apex data type and not the Contestant__c object. You can also see that it's much easier to see what information is available and what is not:

List<ContestantsSelector.Summary> summaries =
  new ContestantsSelector().
    selectByRaceIdWithContestantSummary(raceIds).values();
for(ContestantsSelector.Summary summary : summaries) {
  System.debug(
    summary.Season + ' ' + 
    summary.Race + ' ' + 
    summary.Position + ' ' + 
    summary.Driver + ' ' + 
    summary.Team + ' ' + 
    summary.Car);
}

Depending on your data, this would output something like the following debug:

USER_DEBUG|[26]|DEBUG|2016 Spa 1 Lewis Hamilton Mercedes MP4-29
USER_DEBUG|[26]|DEBUG|2016 Spa 2 Rubens Barrichello Williams FW36

The Selector method returns a new Apex inner class that uses Apex properties to explicitly expose only the field information queried, thus creating a clearer contract between the Selector method and the caller.

It is now not possible to reference the information that has not been queried and would result in runtime errors when code attempts to reference fields that are not queried. The following shows an Apex inner class that the Selector uses to explicitly expose only the information queried by a custom Selector method:

public class Summary {
  private Contestant__c contestant;
  public String Season { 
    get { return contestant.Race__r.Season__r.Name; } }
  public String Race { 
    get { return contestant.Race__r.Name; } }
  public Decimal Position { 
    get { return contestant.RacePosition__c; } } 
  public String Driver { 
    get { return contestant.Driver__r.Name; } }
  public String Team { 
    get { return contestant.Driver__r.Team__r.Name; } }
  public String Car { 
    get { return contestant.Car__r.Name; } }
  private Summary(Contestant__c contestant) 
   { this.contestant = contestant; }
}

The Summary class contains an instance of the query data privately, so no additional heap is used up. It exposes the information as read only and makes the constructor private, indicating that only instances of this class are available through the ContestantsSelector class.

Shown in the following code is the Selector method that performs the actual query:

public Map<Id, List<Summary>>selectByRaceIdWithContestantSummary(Set<Id>raceIds) {

Map<Id, List<Summary>>summariesByRaceId =new Map<Id, List<Summary>>();

for(Contestant__c contestant : Database.query(
    newQueryFactory(false).
      selectField(Contestant__c.RacePosition__c).
      selectField('Race__r.Name').
      selectField('Race__r.Season__r.Name').
      selectField('Driver__r.Name').
      selectField('Driver__r.Team__r.Name').
      selectField('Car__r.Name').
      setCondition('Race__c in :raceIds').
      toSOQL())){
    List<Summary> summaries =summariesByRaceId.get(contestant.Race__c);
    if(summaries==null) {
      summariesByRaceId.put(
      contestant.Race__c, summaries = new List<Summary>());
      summaries.add(new Summary(contestant));
    }
  }

  return summariesByRaceId;
}

Tip

You can utilize the Apex Describe API to add compile-time checking and referential integrity into the preceding relationship references. For example, instead of hardcoded Race__r.Name you can use the following:

Contestant__c.Race__c.getDescribe().getRelationshipName() + '.' + Race__c.Name

The preceding code shows the newQueryFactory method being passed false. This instructs the base class not to configure the query factory returned with the selector fields. This allows the caller to use the selectField method to specify particular fields as required.

Tip

If you are implementing an Apex interface which references custom Selectors method using custom datasets, as described in this section, you should consider creating the class as a top-level class. This avoids potential circular dependencies occurring between the interface and selector class.

Combining Apex data types with SObject types

It is possible to combine Apex data types with SObject data types in responses from Selector methods. So long as when you return SObject data types, they are populated according to the fields indicated by the corresponding Selector.

For example, let's say we want to return an entire Driver__c record associated with the Contestant object, instead of just the Driver name. The change to the Summary Apex class will be to expose the queried Driver__c record, which is safe as it has been populated in accordance with the DriversSelector. The following code shows how the Summary class can be modified to expose the Driver__c record as queried in the custom Selector method:

public class Summary {
  public Driver__c Driver {  
   get { return contestant.Driver__r; } } ...}

In this example a DriversSelector instance is used to obtain the field list, which is injected into the query factory along with other specific fields:

fflib_QueryFactory contestantQueryFactory = newQueryFactory(false).
    selectField(Contestant__c.RacePosition__c).
    selectField('Race__r.Name').
    selectField('Race__r.Season__r.Name').
    selectField('Car__r.Name').
    setCondition('Race__c in :raceIds'),

new DriversSelector().
  configureQueryFactoryFields(
  contestantQueryFactory,
  Contestant__c.Driver__c.getDescribe().getRelationshipName());

Map<Id, List<Summary>> summariesByRaceId =
  new Map<Id, List<Summary>>();
for(Contestant__c contestant :
   Database.query(contestantQueryFactory.toSOQL())) {

SOSL and Aggregate SOQL queries

SOQL is not the only way to query information from the database. Force.com also provides a power Salesforce Object Search Language (SOSL) facility. Like SOQL, this returns SObjects. While this chapter has not covered this variation in depth, the use of Selector methods encapsulating SOSL is appropriate and in fact provides a good abstract from the caller, allowing the developer of the Selector to use SOQL or SOSL in future without impacting the callers.

Likewise, Aggregate SOQL queries are also good candidates to encapsulate in Selector methods. However, in these cases, consider using Apex native data types (for example, a list of values) or lists of custom Apex data types to expose the aggregate information.

Note

The consolidated source code for this book, available in the master branch of the GitHub repository, contains a RaceDataSelector class. Within this class is a custom Selector method, selectAnaysisGroupByRaceName, that demonstrates the use of a SOQL aggregate query.

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

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