Link Search Menu Expand Document

A utility class I briefly referenced in this article was SObjectUnitOfWork. I promised in that article to discuss it in more detail, that time has come! Its main goals are.

  • Optimise DML interactions with the database
  • Provide transactional control
  • Simplify complex code that often spends a good portion of its time managing bulkificaiton and  ‘plumbing’ record relationships together.

In this blog I’m going to show you two approaches to creating a reasonably complex set of records on the platform, comparing the pros and cons of the traditional approach vs that using the Unit of Work approach.

Complex Data Creation and Relationships

Lets first look at a sample peace of code to create a bunch Opportunity records and related, Product , PricebookEntry and eventually OpportunityLine records. It designed to have a bit of a variable element to it, as such the number of lines per Opportunity and thus Products varies depending on which of the 10 Opportunties is being processed. The traditional approach is to do this a stage at a time, creating things and inserting things in the correct dependency order and associating child and related records via the Id’s generated by the previous inserts. Lists are our friends here!

 List<Opportunity> opps = new List<Opportunity>();  
 List<Product2> productsByOpp = new List<Product2>();  
 List<PricebookEntry> pricebookEntriesByOpp = new List<PricebookEntry>();  
 List<OpportunityLineItem> oppLinesByOpp = new List<OpportunityLineItem>();  
 for(Integer o=0; o<10; o++)  
 {  
    Opportunity opp = new Opportunity();  
    opp.Name = 'NoUoW Test Name ' + 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 = pb.Id;  
        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 allProducts = new List();  
for(List products : productsByOpp)  
{  
    allProducts.addAll(products);  
}  
insert allProducts;  
// Insert Pricebooks  
Integer oppIdx = 0;  
List allPricebookEntries = new List();  
for(List pricebookEntries : pricebookEntriesByOpp)  
{  
    List 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 allOppLineItems = new List();  
for(List oppLines : oppLinesByOpp)  
{  
    List 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;  

Lists and Maps (if your linking existing data) are important tools in this process, much like SOQL, its bad news to do DML in loops, as you only get 150 DML operations per request before the governors blow. So we must index and list items within the various loops to ensure we are following best practice for bulkificaiton of DML as well.  If your using ExternalId fields, you can avoid some of this, but to much use of those comes at a cost as well, and your not always able to add these to all objects, so traditionally the above is pretty much the most bulkified way of inserting Opportunities.

Same again, but with a Unit Of Work…

Now thats take a look at the same sample using the Unit Of Work approach to capture the work and commit it all to the database in one operation. In this example notice first of all its a lot smaller and hopefully easier to see what the core purpose of the logic is. Most notable is that there are no maps at all, and also no direct DML operations, such as insert. 

Instead the code registers the need for an insert with the unit of work, for it to perform later via the registerNew methods on lines 8,13,19 and 24. The unit of work is keeping track of the lists of objects and is also providing a kind of ‘stitching’ service for the code, see lines 19, 23 and 24. Because it is given a list of object types when its constructed (via MY_SOBJECT) and these are in dependency order, it knows to insert records its given in that order and then follow up populating the indicated relationship fields as it goes. The result I think is both making the code more readable and focused on the task at hand.

fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS);  
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 = pb.Id;  
        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);  
    }  
}  
uow.commitWork();  

The MY_SOBJECT variable is setup as follows, typically you would probably just have one of these for your whole app.

 // SObjects (in order of dependency)  
 private static List<Schema.SObjectType> MY_SOBJECTS =  
    new Schema.SObjectType[] {  
        Product2.SObjectType,  
        PricebookEntry.SObjectType,  
        Opportunity.SObjectType,  
        OpportunityLineItem.SObjectType };  

Looking into the Future with registerNew and registerRelationship methods

These two methods on the SObjectUnitOfWork class allow you to see into the future. By allowing you to register relationships without knowing the Id’s of records your inserting (also via the unit of work). As you can see in the above example, its a matter of providing the relationship field and the related record. Even if the related record does not yet have an Id, by the time the unit of work has completed inserting dependent records for you, it will. At this point, it will set the Id on the indicated field for you, before inserting the record.

Delegating this type of logic to the unit of work, avoids you having to manage lists and maps to associate related records together and thus keeps the focus on the core goal of the logic.

Note: If you have some cyclic dependencies in your schema, you will have to either use two separate unit of work instances or simply handle this directly using DML.

Deleting and Updating Records with a Unit Of Work…

This next example shows how the unit of work can be used in a editing scenario, suppose that some logic has taken a bunch of OpportunityLineItem’s and grouped them. You need to delete the line items no longer required, insert the new grouped line and also update the Opportunity to indicate the process has taken place.

 // Consolidate Products on the Opportunities  
 fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS);  
 for(Opportunity opportunity : opportunities)  
 {  
    // Group the lines  
    Map<Id, List> linesByGroup = new Map<Id, List>();  
    // Grouping logic  
    // ...  
    // For groups with more than one 1 line, delete those lines and create a new consolidated one  
    for(List linesForGroup : linesByGroup.values() )  
    {  
        // More than one line with this product?  
        if(linesForGroup.size()>1)  
        {  
            // Delete the duplicate product lines and caculate new quantity total  
            Decimal consolidatedQuantity = 0;  
            for(OpportunityLineItem lineForProduct : linesForGroup)  
            {  
                consolidatedQuantity += lineForProduct.Quantity;  
                uow.registerDeleted(lineForProduct);  
            }  
            // Create new consolidated line  
            OpportunityLineItem consolidatedLine = new OpportunityLineItem();  
            consolidatedLine.Quantity = consolidatedQuantity;  
            consolidatedLine.UnitPrice = linesForGroup[0].UnitPrice;  
            consolidatedLine.PricebookEntryId = linesForGroup[0].PricebookEntry.Id;  
            uow.registerNew(consolidatedLine, OpportunityLineItem.OpportunityId, opportunity);  
            // Note the last consolidation date  
            opportunity.Description = 'Consolidated on ' + System.today();  
            uow.registerDirty(opportunity);  
        }  
    }  
 }  
 uow.commitWork();  

Transaction management and the commitWork method

Database transactions is something you rarely have to concern yourself within Apex…. or do you? Consider the sample code below, in it there is a deliberate bug (line 22). When the user presses the button associated with this controller method, the error occurs, is caught and is displayed to the user via the apex:pagemessages component. If the developer did not do this, the error would be unhandled and the standard Salesforce white page with the error text displayed would be shown to the user, hardly a great user experience.

 public PageReference doSomeWork()  
 {  
    try  
    {  
        Opportunity opp = new Opportunity();  
        opp.Name = 'My New Opportunity';  
        opp.StageName = 'Open';  
        opp.CloseDate = System.today();  
        insert opp;  
        Product2 product = new Product2();  
        product.Name = 'My New Product';  
        insert product;  
        // Insert pricebook  
        PricebookEntry pbe = new PricebookEntry();  
        pbe.UnitPrice = 10;  
        pbe.IsActive = true;  
        pbe.UseStandardPrice = false;  
        pbe.Pricebook2Id = [select Id from Pricebook2 where IsStandard = true].Id;  
        pbe.Product2Id = product.Id;  
        insert pbe;  
        // Fake an error  
        Integer x = 42 / 0;  
        // Insert opportunity lines...  
        OpportunityLineItem oppLineItem = new OpportunityLineItem();  
        oppLineItem.Quantity = 1;  
        oppLineItem.TotalPrice = 10;  
        oppLineItem.PricebookEntryId = pbe.Id;  
        insert oppLineItem;  
    }  
    catch (Exception e)  
    {  
        ApexPages.addMessages(e);  
    }  
    return null;  
}  

However using try/catch circumvents the standard Apex transaction rollback during error conditions. “Only when all the Apex code has finished running and the Visualforce page has finished running, are the changes committed to the database. If the request does not complete successfully, all database changes are rolled back.”. Therefore catching the exception results in the request to complete successfully, thus the Apex runtime commits records that lead up to the error occurring. This results in the above code leaving an Opportunity with no lines on the database.

The solution to this problem, is to utilise a Savepoint, as described in the standard Salesforce documentation. To avoid the developer having to remember this, the SObjectUnitOfWork commitWork method creates a Savepoint and manages the rollback to it, should any errors occur. After doing so, it throws again the error so that the caller can perform its own error handling and reporting. This gives a consistant behaviour to database updates regardless of how errors are handled by the controlling logic.

Note: Regardless of using the commitWork method or manually coding your Savepoint logic, review the statement from the Salesforce documentation regarding Id’s.

Summary

As you can see between the two samples in this blog, there is significant reduction of over half the source lines when using the unit of work. Of course the SObjectUnitOfWork class does have its own processing to perform. Because it is a generic library, its never going to be as optimum as if you would write this code by hand specifically for the use case needed as per the first example.

When writing Apex code, optimisation of statements is a consideration (consider Batch Apex in large volumes). However so are other things such as queries, database manipulation and balancing overall code complexity, since smaller and simpler code bases generally contains less bugs. Ultimately, using any utility class, needs to be considered on balance with a number of things and not just in isolation of one concern. Hopefully you now have enough information to decide for yourself if the benefit is worth it in respect to the complexity of the code your writing.

Unit Of Work is a Enterprise Application Architecture pattern by Martin Fowler.