Apply Unit of Work Principles in Apex

Learning Objectives

After completing this unit, you’ll be able to:

  • Describe the Unit of Work class and its methods.
  • Utilize the fflib_SObjectUnitOfWork class and its API in Apex.

Stretching Our Legs with the Unit of Work

Since this is advanced content it only makes sense to look at a more complex scenario: creating an Opportunity and all the required dependent records from scratch, which is surprisingly quite a lot! The following test setup code shows how this could be done without the Unit of Work. Rather verbose, right?

List<Opportunity> opps = new List<Opportunity>();
List<List<Product2>> productsByOpp = new List<List<Product2>>();
List<List<PricebookEntry>> pricebookEntriesByOpp = new List<List<PricebookEntry>>();
List<List<OpportunityLineItem>> oppLinesByOpp = new List<List<OpportunityLineItem>>();

for(Integer o=0; o<10; o++) {

    Opportunity opp = new Opportunity();
    opp.Name = 'Opportunity ' + o;
    opp.StageName = 'Open';
    opp.CloseDate = System.today();
    opps.add(opp);

    List<Product2> products = new List<Product2>();
    List<PricebookEntry> pricebookEntries = new List<PricebookEntry>();
    List<OpportunityLineItem> oppLineItems = new List<OpportunityLineItem>();

    for(Integer i=0; i<o+1; i++) {

        Product2 product = new Product2();
        product.Name = opp.Name + ' : Product : ' + i;
        products.add(product);

        PricebookEntry pbe = new PricebookEntry();
        pbe.UnitPrice = 10;
        pbe.IsActive = true;
        pbe.UseStandardPrice = false;
        pbe.Pricebook2Id = Test.getStandardPricebookId();
        pricebookEntries.add(pbe);

        OpportunityLineItem oppLineItem = new OpportunityLineItem();
        oppLineItem.Quantity = 1;
        oppLineItem.TotalPrice = 10;
        oppLineItems.add(oppLineItem);

    }

    productsByOpp.add(products);
    pricebookEntriesByOpp.add(pricebookEntries);
    oppLinesByOpp.add(oppLineItems);

}

// Insert Opportunities
insert opps;

// Insert Products
List<Product2> allProducts = new List<Product2>();
for(List<Product2> products : productsByOpp) {
    allProducts.addAll(products);
}
insert allProducts;

// Insert Pricebooks
Integer oppIdx = 0;
List<PricebookEntry> allPricebookEntries = new List<PricebookEntry>();
for(List<PriceBookEntry> pricebookEntries : pricebookEntriesByOpp) {

    List<Product2> products = productsByOpp[oppIdx++];
    Integer lineIdx = 0;
    for(PricebookEntry pricebookEntry : pricebookEntries) {
        pricebookEntry.Product2Id = products[lineIdx++].Id;
    }
    allPricebookEntries.addAll(pricebookEntries);

}
insert allPricebookEntries;

// Insert Opportunity Lines
oppIdx = 0;
List<OpportunityLineItem> allOppLineItems = new List<OpportunityLineItem>();

for(List<OpportunityLineItem> oppLines : oppLinesByOpp) {

    List<PricebookEntry> pricebookEntries = pricebookEntriesByOpp[oppIdx];
    Integer lineIdx = 0;

    for(OpportunityLineItem oppLine : oppLines) {
        oppLine.OpportunityId = opps[oppIdx].Id;
        oppLine.PricebookEntryId = pricebookEntries[lineIdx++].Id;
    }

    allOppLineItems.addAll(oppLines);
    oppIdx++;

}

insert allOppLineItems;

Now let’s redo the above code using the Unit of Work pattern. First, start by creating a Unit of Work instance. Remember that the objects passed to the constructor must be in dependency order for the commitWork method to insert them in the correct order.

// Create a Unit Of Work
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(
    new Schema.SObjectType[] {
        Product2.SObjectType,
        PricebookEntry.SObjectType,
        Opportunity.SObjectType,
        OpportunityLineItem.SObjectType
    }
);

In the previous unit, the applyDiscount service method used the registerDirty method to register Opportunity and Opportunity Line records to update. The registerNew method inserts new records. When inserting child records, this method also ensures that the correct parent id is applied before inserting the child records. You can see this in action on line 21. If you have other relationships to make, you can also call the registerRelationship method (line 20). Now let’s put our Unit of Work to work! Pun intended.

// Do some work!
for(Integer o=0; o<10; o++) {

    Opportunity opp = new Opportunity();
    opp.Name = 'UoW Test Name ' + o;
    opp.StageName = 'Open';
    opp.CloseDate = System.today();
    uow.registerNew(opp);

    for(Integer i=0; i<o+1; i++) {

      Product2 product = new Product2();
      product.Name = opp.Name + ' : Product : ' + i;
      uow.registerNew(product);

      PricebookEntry pbe = new PricebookEntry();
      pbe.UnitPrice = 10;
      pbe.IsActive = true;
      pbe.UseStandardPrice = false;
      pbe.Pricebook2Id = Test.getStandardPricebookId();
      uow.registerNew(pbe, PricebookEntry.Product2Id, product);

      OpportunityLineItem oppLineItem = new OpportunityLineItem();
      oppLineItem.Quantity = 1;
      oppLineItem.TotalPrice = 10;

      uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe);
      uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp);

    }

}

As per the earlier unit, these methods do not perform database operations. To do that, you must commit the work using the commitWork method, which calls the correct DML statements with the records that have been registered in the correct order of child and then parent. In addition, for relationships, it assigns the parent Id into the child records., effectively stitching your object model together as it’s inserted into the database.

// Commit the work to the database!
uow.commitWork();

Notice that the code is not only shorter but much easier to read in terms of what is going on logic-wise without all those pesky lists around! Here are more examples of the register methods that you can use.

 // Inserts new Opportunity when committing
uow.registerNew(opp);

// Inserts new Opportunity Line Item and associates it with the given Opportunity record when committing
uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp);

// Relates the given Opportunity Line Item to the given Price Book Entry when committing
uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe);

Summary

In this module, you learned about the benefits of viewing your application implementation in layers that have their own considerations and concerns. Just like any organism, each plays its part in making your application robust and last longer.

You made the first step in that path by isolating the beating heart of your application, its business logic. You can stop here or you can continue to apply separation concerns to object behavior (trigger code) and querying information that your application needs. Make sure you check out the Apex Enterprise Patterns-Service Layer module which continues with the domain and selector application layers. Meanwhile, enjoy building your services!

Preparation for the Challenges

To complete these challenges, you need to deploy some open source libraries. The fflib_SObjectUnitOfWork class is part of the Apex Common open source library which is dependent upon the ApexMocks Framework open source library. You'll need to install ApexMocks first and then Apex Commons. You can read more about both of these libraries and their respective open source license agreements in their repos.

To install the libraries into your org, simply use the "Deploy" buttons below.

Deploy the ApexMocks open source library.

Deploy to Salesforce

Deploy the Apex Common open source library.

Deploy to Salesforce

Resources

retargeting