Skip to content
Home » Blog » UoW’s Dirty Little Secret

UoW’s Dirty Little Secret

Did you know that you have to be super careful with registering records as dirty with a Unit of Work? If you happen to register the same record a second time you can lose your original registered updates. This is especially likely when you are trying to use the UOW to help your big’ol trigger handler be a bit less recursive.

Yes, that is a bit obvious, I know. But.. If (like me) you come up with an idea to not just register the Trigger.new SObject instance and instead pass down a new SObject with only the interesting fields.. Be warned that this does not work!

public void doSomeFancySfuff(fflib_ISObjectUnitOfWork uow) {
    for (MyObject__c record : this.getRecords()) {
        record.CustomField1__c = evaluateStuff(record);
        record.CustomField2__c = evaluateMoreStuff(record);

        uow.registerDirty(
            new MyObject__c(
                Id = record.Id, 
                CustomField1__c = record.CustomField1__c, 
                CustomField2__c = record.CustomField2__c
             );
        );
    }
}

The registered dirty record will still get completely replaced by the newly registered SObject instance with the same Id. The only way to only register specific fields is to use the overloaded version of registerDirty with a second argument: list of SObjectField.

public void doSomeFancySfuff(fflib_ISObjectUnitOfWork uow) {
    for (MyObject__c record : this.getRecords()) {
        record.CustomField1__c = evaluateStuff(record);
        record.CustomField2__c = evaluateMoreStuff(record);

        uow.registerDirty(
            record,
            new List<SObjectField> {
                MyObject__c.CustomField1__c, 
                MyObject__c.CustomField2__c
            )
        );
    }
}

Now this is not as obvious anymore. Or it wasn’t to me. True, it is very much my own fault for not properly learning what the package code I’m using is doing. Ideally I wouldn’t have to though and this wasn’t screaming at me from any of the docs and examples that I got my hands on so far.

The more I think about it the more it makes sense of course. It’s giving us both the options to “hard replace” or “merge” depending on what’s appropriate at the given use case. So far every time I think something in this awesome package is a bit strange I end up arriving at the conclusion that I’m just a few steps behind.

Let’s close this by looking at the bit of code in Unit of Work implementation that we are talking about here (the code is taken from the Apex Common repository but with different formatting applied). Pay attention specifically to line 37.

/**
 * Register an existing record to be updated during the commitWork method
 *
 * @param record An existing record
 **/
public void registerDirty(SObject record) {
    registerDirty(record, new List<SObjectField>());
}

/**
 * Registers the entire records as dirty or just only the dirty fields if the record was already registered
 *
 * @param records SObjects to register as dirty
 * @param dirtyFields A list of modified fields
 */
public void registerDirty(List<SObject> records, List<SObjectField> dirtyFields) {
    for (SObject record : records) {
        registerDirty(record, dirtyFields);
    }
}

/**
 * Registers the entire record as dirty or just only the dirty fields if the record was already registered
 *
 * @param record SObject to register as dirty
 * @param dirtyFields A list of modified fields
 */
public void registerDirty(SObject record, List<SObjectField> dirtyFields) {
    if (record.Id == null)
        throw new UnitOfWorkException('New records cannot be registered as dirty');
    String sObjectType = record.getSObjectType().getDescribe().getName();

    assertForNonEventSObjectType(sObjectType);
    assertForSupportedSObjectType(m_dirtyMapByType, sObjectType);

    // If record isn't registered as dirty, or no dirty fields to drive a merge
    if (!m_dirtyMapByType.get(sObjectType).containsKey(record.Id) || dirtyFields.isEmpty()) {
        // Register the record as dirty
        m_dirtyMapByType.get(sObjectType).put(record.Id, record);
    } else {
        // Update the registered record's fields
        SObject registeredRecord = m_dirtyMapByType.get(sObjectType).get(record.Id);

        for (SObjectField dirtyField : dirtyFields) {
            registeredRecord.put(dirtyField, record.get(dirtyField));
        }

        m_dirtyMapByType.get(sObjectType).put(record.Id, registeredRecord);
    }
}

Other Traps

LWC in Purgatory

Jun 12, 202402 min read

If you are delivering your SFDX projects using packaging you are likely aware of the challenges with moving metadata artefacts from one package to another. Particularly when moving up the dependency hierarchy.  There is this very neat “trick” for Unlocked Packages where you can temporarily remove metadata from a package…

Accidental Permissions

Apr 26, 202402 min read

Blowing the “Single Profile” approach out of the water When you install a package from AppExchange via the browser you get to choose whether to Install it for All Users, Only Admins, Specific Profiles or no-one. This really means if you want to grant full access to Classes, Pages, Fields…

0 0 votes
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Pavel Krušina
Pavel Krušina
1 year ago

Thanks for the warning. So basically the meaning of empty dirty field set is (quite artificially for me) changed to the complete set of all fields.

trackback
UoW and SObject Instances - Pragmatic Bear
1 year ago

[…] 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 happen to […]

2
0
Would love your thoughts, please comment.x
()
x