Skip to content
Home » Blog » DmlWork a.k.a. the UoW meets Triggers

DmlWork a.k.a. the UoW meets Triggers

TL; DR;

Building a neat UoW wrapper for compounding standalone service methods effectively and merging the Unit of Work approach together with updating SObject instances in Triggers. Full code on GitHub.

More posts about UoW

Units of Fun

TL; DR; Peeping into the Unit of Work to help with Batch job chaining and having to really think about who extends who when mocking and testing. Problem The Unit of Work is perhaps the easiest concept from the Enterprise…

UoW and SObject Instances

I previously talked about the need to be careful about how you register records as dirty with the Apex Common’s Unit of Work. There is another little gotcha to be ware of I recently ran into. Though when it does…


Overloaded

Often I need to call the same logic from multiple contexts. Typical example would be Before Trigger logic being called from After trigger of another object or a batch job, only this time including a DML. In my case updating some specific fields on child records when other relevant fields are changed on them or their parent record.

I’ve run into this a number of times. Most recently when gradually refactoring a very trigger heavy org. One avenue was adding a shared Unit of Work to the methods called across a Trigger Handler to save on DMLs. In other cases the approach was to take the feature out of the trigger completely to create a Service called from an Action. Only there were often scenarios where that also had to happen automatically (still from the trigger). 

To cut the long story short I often ended up with multiple versions of the same method. Something like this:

public void doSomething(Id recordId) {
    UoW uow = UoW.newInstance();
    doSomething(recordId);
    uow.commitWork();
}

public void doSomething(Id recordId, UoW uow) {
    CustomObject__c record = CustomObjectSelector.byId(recordId);
    doSomething(record, uow);
}

public void doSomething(CustomObject__c record) {
    doSomething(record, null);
}

private void doSomething(CustomObject__c record, UoW uow) {
    ...
 
    if(uow != null) {
        record.Field__c = ‘Value’;
    } 
    else {
        uow.registerDirty(
            new CustomObject(
                Id = record.Id,
                Field__c = ‘Value’
            ),
            new List<SObjectField>{ CustomObject__c.Status__c }
        );
     }
}

You don’t want to have the same “business” logic implemented multiple times. But supporting the UoW approach and before trigger often leads to ugly noise in the form of null checks on the UoW.

Unit of (DML or Update) Work

I wanted to try and simplify things and came up with a wrapper class around the UoW that really needs a better name. For now let’s call it DmlWork. I think it worked quite well, though I’ve not stayed on the project long enough to judge properly. Now I have to find another home for it. I’d be interested to know what you think about the approach.

Check out the full code on my GitHub). Here I include just the jist of it:

public with sharing class DmlWork {
    private enum Option {
        UPDATE_REFERENCE,
        REGISTER_WORK,
        COMMIT_WORK
    }
    
    private IUnitOfWork uow;
    private Option selectedOption;
    ...

    public IUnitOfWork getUow() {
        return this.uow;
    }

    public void commitIfNeeded() {
        if (this.selectedOption == Option.COMMIT_WORK) {
        this.uow.commitWork();
    }
}

The concept is simple. The class is supposed to help methods support any (or all) of the three approaches to “applying” record changes in a single method version. Where possible keeping with the concept of a single all-or-nothing commit – the Unit of Work.

  • Commit Work – perform any DML required directly inside the method
  • Register Work – register changes to records with a Unit of Work to be committed later
  • Update Reference – update the passed in SObject instance variables (a.k.a. the before trigger)

Only one method instead of overloaded versions means smaller and cleaner classes. The overhead is that each service method may need one of the following extra constructs. When it does not support any one of the above contexts, an assert should be in place to make sure it was called in an acceptable way. (Don’t forget to add a unit test which covers this scenario to document the intent). Also, in order for Commit Work to be supported there needs to be a call to actually do that commit. There is a dedicated method for that which will only perform the commit if it is supposed to happen.

public void complexServiceMethod(Id recordId, DmlWork work) {
    work.assertNotUpdateReference(); 
    //work.assertNotCommitWork();
    //work.assertNotRegisterWork();
    ...
    work.commitIfNeeded();
}

There are of course static init methods available making calling code clear.

MyService.newInstance().doWork(recordsToProcess, DmlWork.commitWork());
MyService.newInstance().doWork(recordsToProcess, DmlWork.registerWork(previouslyCreatedUow));
MyService.newInstance().doWork(recordsToProcess, DmlWork.updateReference());

Silent UoW

The Update Reference context uses a dummy (or silent) unit of work so that the code in the method does not have to check for UoW not being null. However, there still is a bit of a faffing to do to put these changes both in the SObject instance and the UoW to make the Update Reference context work together with the other two.

public void complexServiceMethod(CustomObject__c record, DmlWork work) {
    ...
    //for Update Reference context
    record.Status__c = ‘Changed Status’;
    //for UoW contexts
    work.getUow().registerDirty(
        new CustomObject(
            Id = record.Id,
            Status__c = ‘Changed Status’
        ),
        new List<SObjectField>{ CustomObject__c.Status__c }
    );
    work.commitIfNeeded();
}

If you wonder why I’m not passing in the record SObject variable directly to the registerDirty method, I talk about that here.

I didn’t like that so I’ve added a method wrapping these 2 things together which makes the “client” code a lot neater.

//in DmlWork
public void set(SObject record, SObjectField field, Object value) {
    SObject cloneRecord = record.getSObjectType().newSObject(record.Id);
		record.put(field, value);
		cloneRecord.put(field, value);
		this.uow.registerDirty(cloneRecord, new List<SObjectField>{ field });
}

//reworked service method
public void complexServiceMethod(CustomObject__c record, DmlWork work) {
    ...
    work.set(record, CustomObject__c.Status__c, 'Changed Status');
    work.commitIfNeeded();
}

The intention for this wasn’t primarily Update Reference contexts though. It is very useful during the untangling of triggers. But real service methods are typically too complex to support this approach anyway. You can’t support inserting new records for starters. Changing multiple object types is also tricky unless your method argument list is long (bad). And I personally aim to contain work inside the methods instead of leaking side-effects everywhere. If one manages to get over the urge to solve everything with Trigger handlers the Update Reference context becomes almost an anti-pattern.

Compound Services

Combining the other two contexts I found very neat though. Particularly because I ran into several instances of combining Service calls together. An example:

I had an ServerService class with a method to “push” updates to records to an external system. This needed to happen in various situations (changes to key fields, canceling, ..). I also had a sort of VersionService with a method to make changes to a record and its child records in specific ways. Again, this was needed in various situations, not always the same ones. Finally, there was the StatusService with its cancel method. This needed to change some fields, use the VersionService to cancel items and the ServerService to sync this cancellation outside of Salesforce.

Any of the first two services can be called on their own passing in the right version of WorkDml.

public class Controller {
    @AuraEnabled
    public static void doSomething(Id recordId) {
        ...
        ServiceService.newInstance().pushAsync(record, DmlWork.commitWork());
        ...
    }
}

The “outer” service then just needs to apply one (fairly obvious, even if after first failed test) trick. Whenever it’s calling the others it has to explicitly use the Register Work context providing its UoW to tehm even when it itself is called with Commit Work. Otherwise the whole concept of “One Unit of Work” would be broken (and potentially limits compromised).

public void cancel(CustomObject__c record, DmlWork work) {
    ...
    work.set(record, CustomObject__c. record.Status__c, ‘Changed Status’);
    VersionService.newInstance().cancelItems(record, items, WorkType.registerWork(WorkType.getUow()));
    ServiceService.newInstance().pushAsync(record, WorkType.registerWork(work.getUow()));
    ...
    work.commitIfNeeded();
    ...
}

What do you think?

And that’s it. I was quite happy with myself when I put this together :-). It genuinely seemed useful both when reworking Trigger-called overloaded methods and combining Services into more complex actions. Can you see yourself using something similar to this?

Where to next

Eliška Bušáková

Eliška Bušáková

Sep 8, 20226 min read

TL; DR; WiT community group leader Eliška Bušáková recommends to not be afraid to face challenges head on. Intro Hi,…

A very responsible api developer who does not make breaking changes in their packages

Moving Metadata Between Packages

Nov 6, 20244 min read

The dangers of delayed compilation TL; DR; I’ve previously mentioned a handy way to allow moving components between packages during…

5 3 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x