Implementing Domain Trigger logic

The most common initial use case for a Domain class is to encapsulate the Apex Trigger logic. In order to enable this, a small Apex Trigger is required to invoke the triggerHandler method. This will route the various Trigger events to the appropriate methods in the Domain class (as shown in the preceding screenshot), avoiding the need for the usual if/else logic around Trigger.isXXXX variables.

The name of this trigger can be anything, though it makes sense to match it with that of the corresponding Domain class. Once this is in place, you can ignore it and focus on implementing the Domain class methods as follows:

trigger Seasons on Season__c (
  after delete, after insert, after update, 
  before delete, before insert, before update) {
    fflib_SObjectDomain.triggerHandler(Seasons.class);
}

Routing trigger events to Domain class methods

The following diagram illustrates the flow of execution from the Apex Trigger to the Domain class, triggerHandler, to the individual methods:

Routing trigger events to Domain class methods

To help you understand how traditional Apex Trigger events are mapped to the various virtual methods in the base class, this table describes the code path taken from when the base class's fflib_SObjectDOmai.triggerHandler method is invoked through the specific event-based methods that can be overridden by a Domain class:

Trigger context

Base class Records property value

Base class handle method called (refer to the following note)

Base class event method(s) called (refer to the following note)

Trigger.isBefore

   

Trigger.isInsert

Trigger.new

handleBeforeInsert()

onApplyDefaults()onBeforeInsert()

Trigger.isUpdate

Trigger.new

handleBeforeUpdate(Map<Id,SObject>existingRecords)

onBeforeUpdate(Map<Id,SObject>existingRecords)

Trigger.isDelete

Trigger.oldMap

handleBeforeDelete()

onBeforeDelete()

Trigger.isAfter

   

Trigger.isInsert

Trigger.new

handleAfterInsert()

onValidate()onAfterInsert()

Trigger.isUpdate

Trigger.new

handleAfterUpdate(Map<Id,SObject>existingRecords)

onValidate(Map<Id,SObject>existingRecords)onAfterUpdate(Map<Id,SObject>existingRecords)

Trigger.isDelete

Trigger.oldMap

handleAfterDelete()

onAfterDelete()

Note: Handle methods can also be overridden if you wish to implement your own handling. The existingRecords parameter represents the value of Trigger.oldMap.

Tip

The base class uses the template method pattern (http://en.wikipedia.org/wiki/Template_method_pattern). This ensures that when overriding the onXXX methods from the base class, you do not need to worry about calling the base class version of the method, as is the typical developer requirement when overriding methods in OOP.

Enforcing object security

Salesforce requires developers to implement the Object CRUD (Create, Read, Update, and Delete) security and Field Level security checks using the methods on the SObjectDescribe and SObjectFieldDescribe Apex runtime types.

By default, the preceding handle methods in the fflib_SObjectDomain base class used in this chapter automatically perform the Object CRUD security on behalf of the developers Domain class logic.

Note

Note that currently the base class still leaves implementing Field Level security to the developer to implement.

A developer controlled configuration option provided by the base class allows each Domain class to control whether this default behavior is enabled or not. For example, objects that are typically maintained by the application code on behalf of the user may not wish to have this check enforced, such as the awarding of championship points. This type of access is sometimes referred as system level access, as the system performs an operation on behalf of the user. Thus by accepting the default to enforce this check, it would require users to be granted access which would not be appropriate elsewhere in the application. Another motivation for not leveraging the default enforcement would be a preference to code these checks in your controller logic more explicitly. Consider carefully the default enforcement is what you need in each case.

Default behavior

The handler method checks the required security before calling the preceding event methods and throws a DomainException exception if the user does not have the required access (forcing the entire trigger context to stop). For example, when the handleAfterInsert method is invoked, the SObjectDescribe.isCreatable method is used.

Overriding the default behavior

To make it easier to configure the fflib_SObjectDomain class throughout the application, as well as to provide a means to establish shared or common Domain logic code, a new base class is created as follows.

/**
 * Application specific Domain base class, *  customisefflib_SObjectDomain and add common behavior
 **/
public abstract class ApplicationDomain extendsfflib_SObjectDomain {
  public ApplicationDomain(List<SObject> records) {
    super(records);
    // Disable CRUD security enforcement at the Domain class level
    Configuration.disableTriggerCRUDSecurity();
  }    
}

Thus Domain classes in this application extended this class instead.

public class Contestants extends ApplicationDomain {
  public Contestants(List<Contestant__c> contestants) {
    super(contestants);
  }

Note

Note that this approach requires that the developer takes over exclusive responsibility for implementing Object CRUD security checking them.

Apex Trigger event handling

The upcoming sections illustrate some Apex Trigger examples with respect to the FormulaForce application. They utilize the Drivers and Contestants Domain classes included in the sample code for this chapter along with the following new fields:

  • In the Driver__c object, a new field called ShortName__c as a Text type, with a maximum size of 3 characters, has been created.
  • In the Race__c object, there is a new field called Status__c as a Picklist type, with the Scheduled, In Progress, Red Flagged, and Finished values. The default value is scheduled by ticking the Use first value as default value checkbox.

Defaulting field values on insert

Although the onInsertBefore method can be overridden to implement logic to set default values on record insert, the following code, added to the Contestants Domain class, has chosen to override the onApplyDefaults method instead. This was used as it is more explicit and permits a Separation of Concerns between the defaulting code and the record insert code, although both are fired during the insert phase:

public override void onApplyDefaults() {
  for(Driver__c driver : (List<Driver__c>) Records) {
    if(driver.ShortName__c == null) {
      // Upper case first three letters of drivers last name
      String lastName = driver.Name.substringAfterLast(' '),
      driver.ShortName__c = lastName.left(3).toUpperCase();
    }
  }
}

The preceding code attempts to populate the Drivers short name, which is typically made up of the first three characters of their second name in upper case, so Lewis Hamilton will become HAM.

Validation on insert

The following validation logic applies during record creation. It has been added to the Contestants Domain class and ensures that contestants are only added when the race is in the Scheduled state.

It also demonstrates the use of a Selector pattern class to bulk load records from the Race__c Custom Object, and this pattern will be covered in detail in the next chapter:

public override void onValidate() {
  // Bulk load the associated races
  Set<Id>raceIds = new Set<Id>();
  for(Contestant__c contestant : (List<Contestant__c>) Records) {
     raceIds.add(contestant.Race__c);}
  Map<Id, Race__c>associatedRaces = 
    new Map<Id, Race__c>(
      new RacesSelector().selectById(raceIds));
  // Only new contestants to be added to Scheduled races
  for(Contestant__c contestant : 
    (List<Contestant__c>) Records) {
    Race__c race = associatedRaces.get(contestant.Race__c);
    if(race.Status__c != 'Scheduled') { contestant.addError('Contestants can only be added to scheduled races'),}   
  } 
}

Tip

By overriding the onValidate method, the logic always invokes the after phase of the Apex Trigger. As no further edits can be made to the record fields, this is the safest place to perform the logic. Always keep in mind that your Apex Trigger might not be the only one assigned to the object. For example, once your package is installed in a subscriber org, a subscriber-created custom trigger on your object might fire before yours (the order of trigger invocation is non-deterministic) and will attempt to modify fields. Thus, for managed package triggers, it is highly recommended that validation be performed in the after phase to ensure complete confidence and security, as the data will not be modified further after validation.

Validation on update

An alternative method, override, is provided to implement the validation logic applicable to record updates. It is an overload of the onValidate method shown in the preceding example, used to pass in the current records on the database for comparison if needed:

public override void onValidate(
  Map<Id,SObject>existingRecords) {
  // Bulk load the associated races
  Map<Id, Race__c>associatedRaces = queryAssociatedRaces();
  // Can only change drivers in scheduled races
  for(Contestant__c contestant : (List<Contestant__c>) Records) { 
    Race__ccontestantRace =associatedRaces.get(contestant.Race__c);
    Contestant__coldContestant  =(Contestant__c) existingRecords.get(contestant.Id);
    if(contestantRace.Status__c != 'Scheduled' &&contestant.Driver__c !=  oldContestant.Driver__c) {
      contestant.Driver__c.addError(
        'You can only change drivers for scheduled races'),}
  }
}

The preceding code leverages a new custom Domain class method to share the code responsible for loading associated Race records for Contestants across the onValidate method shown in the previous example. This method also uses the Records property to determine Contestants in scope:

private Map<Id, Race__c> queryAssociatedRaces() {
  // Bulk load the associated races
  Set<Id>raceIds = new Set<Id>();
  for(Contestant__c contestant : (List<Contestant__c>) Records) {
    raceIds.add(contestant.Race__c);
  }
  return new Map<Id, Race__c>(new RacesSelector().selectById(raceIds)); 
}

Tip

The preceding method is marked as private, as there is currently no need to expose this functionality to other classes. Initially, restricting methods this way is often considered best practice and permits easier internal refactoring or improvements in the future. If a need arises that needs access to this functionality, the method can be made public.

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

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