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.
Follow Along with Trail Together
Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series.
(This clip starts at the 1:08:31 minute mark, in case you want to rewind and watch the beginning of the step again.)
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 22. If you have other relationships to make, you can also call the registerRelationship method (line 21). 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: Domain & Selector Layers 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 the Apex Common open source library.
Resources
- Separation of Concerns (Wikipedia)
- Martin Fowler’s Enterprise Architecture Patterns
- Martin Fowler’s Unit of Work Patterns
- Apex Enterprise Patterns - GitHub Repo
- Managing your DML and Transactions with a Unit of Work
- Doing more work with the Unit of Work