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.
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.
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.
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
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()); }
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
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:
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; }
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.
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())) {
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.
18.218.151.44