Handling DML with the Unit Of Work pattern

The database maintains relationships between records using record IDs. Record IDs are only available after the record is inserted. This means that the related records, such as child object records, need to be inserted in a specific dependency order. Parent records should be inserted before child records, and the parent record IDs are used to populate the relationship (lookup) fields on the child record objects before they can be inserted.

The common pattern for this is to use the List or Map keyword to manage records inserted at a parent level, in order to provide a means to look up parent IDs, as child records are built prior to being inserted. The other reasoning for this is bulkification; minimizing the number of DML statements being used across a complex code path is vital to avoid hitting governor limits on the number of DML statements required as such lists are favored over executing individual DML statements per record.

The focus on these two aspects of inserting data into objects can often detract from the actual business logic required, making code hard to maintain and difficult to read. The following section introduces another of Martin Fowler's patterns, the Unit Of Work, which helps address this, as well as providing an alternative to the boilerplate transaction management using SavePoint, as illustrated earlier in this chapter.

The following is Martin Fowler's definition of Unit Of Work (http://martinfowler.com/eaaCatalog/unitOfWork.html):

"Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems."

In an extract from the given webpage, he also goes on to make these further points:

"You can change the database with each change to your object model, but this can lead to lots of very small database calls, which ends up being very slow.

A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done, it figures out everything that needs to be done to alter the database as a result of your work."

Although these statements don't appear to relate to Force.com, there are parallels with respect to the cost of making multiple DML statements, both in terms of governors and general performance. There is also a statement about transaction management at a business (or service) level, which applies to the responsibility of the Service layer.

In order to best illustrate these benefits, let's review the implementation of a service to load some historic data into the Season, Race, Driver, and Contest objects. Each of these objects has several relationships, as illustrated in the following screenshot:

Handling DML with the Unit Of Work pattern

Consider the following sample JSON format to import data into the application:

{
  "drivers": [
  {
    "name": "Lewis Hamilton",
    "nationality": "British",
    "driverId": "44",
    "twitterHandle": "lewistwitter"
  }],
  "seasons": [
  {
    "year": "2013",
    "races": [
    {
      "round": 1,
      "name": "Spain",
      "contestants": [
      {
        "driverId": "44",
        "championshipPoints": 44,
        "dnf": false,
        "qualification1LapTime": 123,
        "qualification2LapTime": 124,
        "qualification3LapTime": 125
      }]
    }]
  }]
}

Without a Unit Of Work

This first example shows a traditional approach using Map and List to import the data, obeying bulkification and inserting a dependency order. The important logic highlighted in the following code copies data from the imported data structures into the objects; everything else is purely managing transaction scope and inserting dependencies:

public static void importSeasons(String jsonData) {
  System.SavepointserviceSavePoint = Database.setSavePoint();
  try{
    // Parse JSON data
    SeasonsDataseasonsData = 
       (SeasonsData) JSON.deserializeStrict(jsonData,    
SeasonService.SeasonsData.class);
    // Insert Drivers
    Map<String, Driver__c>driversById = 
      new Map<String, Driver__c>();
    for(DriverDatadriverData : seasonsData.drivers)
      driversById.put(driverData.driverId, new Driver__c(
      Name = driverData.name,
      DriverId__c = driverData.driverId,
      Nationality__c = driverData.nationality,
      TwitterHandle__c = driverData.twitterHandle));
      insert driversById.values();

    // Insert Seasons 
    Map<String, Season__c>seasonsByYear = 
      new Map<String, Season__c>();
    for(SeasonDataseasonData : seasonsData.seasons)
      seasonsByYear.put(seasonData.year,
         new Season__c(Name = seasonData.year, Year__c = seasonData.year));
      insert seasonsByYear.values();

    // Insert Races
    Map<String, Race__c>racesByYearAndRound = 
      new Map<String, Race__c>();
    for(SeasonDataseasonData : seasonsData.seasons)
      for(RaceDataraceData : seasonData.races)
        racesByYearAndRound.put(seasonData.Year + raceData.round,new Race__c(
              Season__c = seasonsByYear.get(seasonData.year).Id,
              Name = raceData.name));
              insert racesByYearAndRound.values();

    // Insert Contestants
    List<Contestant__c> contestants = 
      new List<Contestant__c>();
    for(SeasonDataseasonData : seasonsData.seasons)
    for(RaceDataraceData : seasonData.races)
        for(ContestantDatacontestantData
              : raceData.contestants)
    contestants.add(
        new Contestant__c(
           Race__c = racesByYearAndRound.get(
               seasonData.Year + raceData.round).Id,
           Driver__c = driversById.get(
               contestantData.driverId).Id,
           ChampionshipPoints__c =
               contestantData.championshipPoints,
           DNF__c = contestantData.dnf,
           Qualification1LapTime__c =
               contestantData.qualification1LapTime,
           Qualification2LapTime__c =
               contestantData.qualification2LapTime,
           Qualification3LapTime__c =
               contestantData.qualification3LapTime
        ));
     insert contestants;
  } catch (Exception e) {
    // Rollback any data written before the exception
    Database.rollback(serviceSavePoint);
    // Pass the exception on
    throw e;
  }
}

Note

This data import requirement could have been implemented by using the Salesforce Data Loaders and external field references, as described in the earlier chapter. One reason you may decide to take this approach is to offer an easier option from your own UIs that can be made available to general users without administrator access.

With Unit Of Work

Before we take a closer look at how the FinancialForce Apex Enterprise Pattern library has helped us implement this pattern, lets first take a look at the following revised example of the code shown earlier. This code utilizes a new Apex class called Application. This class exposes a static property and method to create an instance of a Unit Of Work.

Note

The source code for this chapter includes the FinancialForce Apex Enterprise Pattern library, as well as the FinancialForce Apex Mocks library it depends on. The Apex Mocks library will become the focus in a later chapter where we will focus on writing unit tests for each of the patterns introduced in this book.

The fflib_ISObjectUnitOfWork Apex Interface is used to expose the features of the Unit Of Work pattern to capture the database work as records are created, thus the remaining logic is more focused and avoids Map and List and repeated iterations over the imported data shown in the previous example. It also internally bulkifies the work (DML) for the caller. Finally, the commitWork method call performs the actual DML work in the correct dependency order while applying transaction management.

Consider the following code:

  // Construct a Unit Of Work to capture the following working
  fflib_ISObjectUnitOfWorkuow = Application.UnitOfWork.newInstance();

  // Create Driver__c records
  Map<String, Driver__c>driversById = 
  new Map<String, Driver__c>();

  for(DriverDatadriverData : seasonsData.drivers){
    Driver__c driver = new Driver__c(
      Name = driverData.name,
      DriverId__c = driverData.driverId,
      Nationality__c = driverData.nationality,
      TwitterHandle__c = driverData.twitterHandle);
    uow.registerNew(driver);
    driversById.put(driver.DriverId__c, driver);
  }

for(SeasonDataseasonData : seasonsData.seasons){
  // Create Season__c record
  Season__c season = new Season__c(
    Name = seasonData.year,
     Year__c = seasonData.year);
    uow.registerNew(season);
  for(RaceDataraceData : seasonData.races){
    // Create Race__c record
    Race__c race = new Race__c(Name = raceData.name); 
    uow.registerNew(race, Race__c.Season__c, season);
    for(ContestantDatacontestantData : raceData.contestants){
      // Create Contestant__c record
      Contestant__c contestant = new Contestant__c(
        ChampionshipPoints__c = contestantData.championshipPoints,
        DNF__c = contestantData.dnf,
        Qualification1LapTime__c =contestantData.qualification1LapTime,
        Qualification2LapTime__c =contestantData.qualification2LapTime,
        Qualification3LapTime__c =contestantData.qualification3LapTime);
        uow.registerNew (contestant, Contestant__c.Race__c, race);
        uow.registerRelationship(contestant, Contestant__c.Driver__c,driversById.get(contestantData.driverId));
      }
    }
  }
  // Insert records registered with uow above
  uow.commitWork();

The following is the implementation of the Application class and the UnitOfWork static property. It leverages a simple factory class that dynamically creates instances of the fflib_SObjectUnitOfWork class through the newInstance method:

public class Application 
{
  // Configure and create the UnitOfWorkFactory for
  //  this Application
  public static final fflib_Application.UnitOfWorkFactory
  UnitOfWork = new fflib_Application.UnitOfWorkFactory(
  new List<SObjectType> { 
    Driver__c.SObjectType, 
    Season__c.SObjectType, 
    Race__c.SObjectType, 
    Contestant__c.SObjectType 
  });
}

The class fflib_Application.UnitOfWorkFactory exposes the newInstance method that internally creates a new fflib_SObjectUnitOfWork instance. This is not directly exposed to the caller; instead the fflib_ISObjectUnitOfWork interface is returned (this design aids the mocking support we will discuss in a later chapter). The purpose of this interface is to provide the methods used in the preceding code to register records for insert, update, or delete, and implement the commitWork method.

I recommend that you create a single application Apex class like the one shown previously, where you can maintain the full list of objects used in the application and their dependency order; as your application's objects grow, it will be easier to maintain.

The fflib_ISObjectUnitOfWork interface has the following methods in it. The preceding example uses the registerNew and registerRelationship methods to insert records and ensure that the appropriate relationship fields are populated. Review the documentation of these methods in the code for more details. The following screenshot shows a summary of the methods:

With Unit Of Work

Call the registerDirty method with a SObject record you want to update and the registerDeleted method to delete a given SObject record. You can also call a combination of the registerNew, registerDirty, and registerDeleted methods.

The Unit Of Work scope

To make the most effective use of the Unit Of Work's ability to coordinate database updates and transaction management, maintain a single instance of the Unit Of Work within the scope of the Service method, as illustrated in the previous section.

If you need to call other classes that also perform database work, be sure to pass the same Unit Of Work instance to them, rather than allowing them to create their own instance. We will explore this a little later in this chapter.

Tip

It may be tempting to maintain a static instance of Unit Of Work so that you can refer to it easily without having to pass it around. The downside of this approach is that the scope becomes broader than the service layer execution scope as Apex can be invoked within an execution context multiple times. Code registering records with a static execution scope Unit Of Work depending on an outer Apex code unit committing the work. It also gives a false impression that the Unit Of Work is useable after a commit has been performed.

Unit Of Work special considerations

Depending on your requirements and use cases, the standard methods of Unit Of Work may not be appropriate. The following is a list of some use cases that are not handled by default and may require a custom work callback to be registered:

  • Self and recursive referencing: Records within the same object that have lookups to each other are not currently supported, for example, account hierarchies.
  • More granular DML operations: The Database class methods allow the ability to permit some records to be processed when others fail. The DML statements executed in the Unit Of Work are defaulted to all or nothing.
  • Sending e-mails: Sending e-mails is considered a part of the current transaction; if you want to register this kind of work with the Unit Of Work, you can do so via the registerEmail method. Multiple registrations will be automatically bulkified.

The following is a template for registering a customer work callback handler with Unit Of Work. This will be called during the commitWork method within the transaction scope it creates. This means that if work fails in this work callback, it will also call other work registered in the standard way with Unit Of Work to rollback. Take a look at the code:

public class CustomAccountWork implementsfflib_SObjectUnitOfWork.IDoWork {
   private List<Account> accounts;

   public CustomAccountWork (List<Account> accounts) {this.accounts = accounts;
   }

   public void doWork(){
      // Do some custom account work e.g. hierarchies
   }
}

Then, register the custom work as per the following example:

uow.registerNew...
uow.registerDirty...
uow.registerDeleted...
uow.registerWork(new CustomAccountWork(accounts));
uow.commitWork();
..................Content has been hidden....................

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