Andy in the Cloud

From BBC Basic to Force.com and beyond…

Doing more work with the Unit Of Work

16 Comments

In a previous post I introduced the Unit Of Work class, which is part of the Apex Enterprise Patterns series. As the original post describes it helps you simplify your code when it comes to performing multiple DML statements over related objects, as well as providing a bulkification and transaction management. In this post i would like to share information on a new feature that adds some extensibility to the fflib_SObjectUnitOfWork class.

This new feature addresses use cases where the work you want it to do, does not fit with the use of the existing registerDirty, registerNew or registerDeleted methods. But you do want that work to only be performed during the commitWork method, along with other registered work and within thesame transaction it manages.

Some examples of such situations are…

  • Sending Emails
    You want to register the sending an email once the work has completed and have that email only be sent if the entire unit of work completes.
  • Upsert
    You want to perform an upsert operation along with other insert, update and delete operations performed by the existing functionality.
  • Database class methods
    You want to utilise the Database class methods for additional functionality and/or granularity on some database operations, e.g. emptyRecycleBin, convertToLead, or perform an insert with allOrNothing set to false.
  • Self referencing objects
    You want to perform DML work relating to self referencing objects, something which is currently not supported by the registerNew and registerRelationship methods.
  • Nested Unit Of Works
    Though generally not recommended, if you do happen to have created another fflib_SObjectUnitOfWork instance, you might want to nest another unit of work instance commitWork call with another.

In these cases the class now contains a new Apex Interface, called IDoWork, it is an incredibly simple interface!

/**
* Interface describes work to be performed during the commitWork method
**/
public interface IDoWork
{
   void doWork();
}

To use this interface you implement it and register an instance of the implementing class through the new registerWork method. When the commitWork method is called it will callback on the doWork method for each registered implementations once it has completed the work given to it by the existing methods. In other words after all the DML on the dirty, new or deleted records have been processed.

A SendEmailWork implementation of this interface actually resides internally within the fflib_SObjectUnitOfWork class and is used to support another new method called, registerEmail. I added this to experiment with the feature during development but felt its worth leaving in as an added bonus feature if your writing code thats doing DML and sending emails!

The following example integrates the upsert method on the Database class (which needs a concreate SObject list), but you can really do anything you want in the doWork method!

	public class UpsertUnitOfWorkHelper implements fflib_SObjectUnitOfWork.IDoWork
	{ 		
		public Database.UpsertResult[] Results {get; private set;}
		
		private List<Account> m_records;
		
		public UpsertUnitOfWorkHelper()
		{  
			m_records = new List<Account>();
		}
		
		public void registerAccountUpsert(Account record)
		{
			m_records.add(record);
		}
		
		public void doWork()
		{
			Results = Database.upsert(m_records, false);				
		}
	}

The following is an example of this in use…

		// Create Unit Of Work as normal
		fflib_SObjectUnitOfWork uow =
			new fflib_SObjectUnitOfWork( 
				new Schema.SObjectType[] { 
					Product2.SObjectType, 
					PricebookEntry.SObjectType, 
					Opportunity.SObjectType, 
					OpportunityLineItem.SObjectType });
		
		// Register some custom work
		UpsertUnitOfWorkHelper myUpsertWork = new UpsertUnitOfWorkHelper();
		uow.registerWork(myUpsertWork);
				
		// Do standard work via...		
		
		// uow.registerNew(...)
		
		// uow.registerDeleted(...)
		
		// uow.registerDirty(...)
		
		// Register some custom work via...
		myUpsertWork.registerAccountUpsert(myAccountRecordToUpsert);
		
		// Commit work as normal
		uow.commitWork();
		
		// Optionally, determine the results of the custom work...
		List<Database.UpsertResult> myUpsertResults = myUpsertWork.Results;		

In summary, the unit of work helps co-ordinate many things we do that in essence are part of a transaction of work we want to happen as a single unit. This enhancement helps integrate other “work” items your code might have been performing outside the unit of work into that single unit of work and transaction. It also helps bridge some gaps in the unit of work that exist today such as self referencing objects.

Please let me know your thoughts on this new feature and how you see yourself using it!

16 thoughts on “Doing more work with the Unit Of Work

  1. Pingback: Working with Apex Mocks Matchers and Unit Of Work | Andy in the Cloud

  2. Andy — tis unfortunate we can’t do registerUpsert as I ran into the following use case:

    Use Case: Copy Opportunity + OpportunityLineItems to Sales_Order__c and Sales_Order_Item__c (s) where it is possible the Sales Order already exists and has to be replaced without deleting/re-inserting

    uow.registerDelete(list of all Sales Order Items) // these can be wiped out and re-added
    for (Opportunity o: oppoList) {
    Sales_Order__c so = makeSalesOrderFromOpportunity(o,existingSalesOrderIfAny));
    uow.registerUpsert(so); // unfortunately, not doable
    for (OpportunityLineItem oli: o.OpportunityLineItems)
    uow.registerNew(makeSalesOrderItem(oli),Sales_Order_Item__c.Sales_Order__c,so); // add new Sales Order Item as child of Sales Order
    }
    uow.commitWork();

    Resorting to the IDoWork interface and registering your own worker to upsert Sales Order and insert Sales Order Item is aesthetically clumsy as you are mixing some DML done by the fflib_SObjectUnitOfWork and some via native APEX DML in your IDoWork handler.

    The meta comment is that when one dives into the SoC pattern, one wants to use the pattern everywhere and not have to worry about exceptions. I realize there’s not much that can be done here and at least we get the uow to handle the transaction wrapping.

    Sigh.

    • Yeah good to share this experience, agree best to share transaction scope. You could subclass unit of work and add your own specialised methods that hide the idowork impl perhaps?

  3. Hey Andy,
    Just wondering if the registerX() methods could be wrapped in accessor methods, eg. registerDirty in a setFieldX method to mitigate the possibility of a programmer forgetting to registerX in their code.

  4. Hey Andy, I wondered if it was possible to wrap the registerX method in modifiers / accessors, eg. invoke registerDirty() in the setFieldX() method to mitigate a user forgetting to invoke the register method whilst they’re writing code?

    • Best practices are to avoid accessor methods with side effects tbh. It’s better left up to the controlling code, plus sometimes to much automation can hide important considerations and awareness from new developers. Finally writing and maintaining them would also become quite and overhead, assuming one could figure a way to express this, since the domain classes are collections of records. So yeah, on balance, it’s a nice motivation for sure, practically it will likely create more problems than it solves I feel.

  5. I think another useful use case for IDoWork is when you need (in one transaction) to insert an object Foo with a Files related list. This might come up in an email service wherein an object is created and files are created from the email’s attachments.

    This use case involves inserting:

    (1) The Object Foo (registerNew)
    (2) A ContentVersion (registerNew)
    (3) A ContentDocumentLink (uh-oh — escape to doWork)

    Step 3 can’t be done without querying the inserted ContentVersion to obtain the value of field ContentDocumentId as ContentDocumentLink is a junction between ContentDocument (not ContentVersion! ) and the object Foo

  6. Pingback: FinancialForce Apex Common Community Updates | Andy in the Cloud

  7. Hi Andy ,

    We work mostly on pulling data from Interfaces and updating data within SFDC. We can work only with ExxternalID.
    Can we have method in unit of work which can allow upsert using an ExternalID ?

    We are able to do upsert using external id by leveraging above format.

    It will be really helpful.

  8. Hi ,
    Can you please share an Example implementing SendEmailWork class , I cant access the link anymore.

  9. Hi Andi, How do we write the test class for the above implementation by using mock classes for the unit of work.

Leave a comment