Skip to content
Home » Blog » UoW and SObject Instances

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 happen to you then you are probably doing something you should not be doing. It’s all to do with instances of SObject you are passing in.

Let’s say I have a method which modifies a record that was passed in and registers it with the UoW. I call this method but then also continue working with the same record doing other changes to it. Only those later changes may just be some processing that for whatever reason needs to be abandoned (not committed).

public void doComplexWork(List<MyObject__c> records) {
    fflib_ISObjectUnitOfWork uow = ClassFactory.newUOWInstance();
    for(MyObject__c record : records) {
        doSomeStuff(record, uow);
        optionallyDoSomeMoreStuff(record, uow);
    }
    uow.commitWork();
}

//imagine this is in another class somewhere
private void doSomeStuff(MyObject__c record, fflib_ISObjectUnitOfWork uow) {
    record.CustomField__c = evaluateStuff(record); //evaluates to "1"
    uow.registerDirty(
        record,
        new List<SObjectField>{ MyObject__c.CustomField__c }
    );
}

private void optionallyDoSomeMoreStuff(MyObject__c record, fflib_ISObjectUnitOfWork uow) {
    record.CustomField__c = evaluateMoreStuff(record); //evaluates to "2"
    if(record.CustomField__c == SOME_CONSTANT && OTHER_CONDITION) { //evaluates to FALSE
        uow.registerDirty(
            record,
            new List<SObjectField>{ MyObject__c.CustomField__c }
        );
    }
}

What would be the value committed to record.CustomField__c, “1” or “2”? It’s definitely not the intention, but it would be “2”. Because SObjects passed into methods as arguments reference the same instance. So the instance of record passed into the UoW in method doSomeStuff is the same one modified (even though not registered or committed) in method optionallyDoSomeMoreStuff.

I found myself protecting my code from such unintended modifications by always registering “cloned” instances of records with the UoW to be sure any such reference links are broken. I think intentionally registering a record to do updates that may or may not happen later is asking for trouble.

private void doSomeStuff(MyObject__c record, fflib_ISObjectUnitOfWork uow) {
    record.CustomField__c = evaluateStuff(record); //evaluates to "1"
    uow.registerDirty(
        new MyObject__c (
            Id = record.Id,
            CustomField__c = record.CustomField__c
        ),
        new List<SObjectField>{ MyObject__c.CustomField__c }
    );
}

This is a bit ugly, I know. And doing this every time the UoW is used seems wrong. In a clean org and diligent team this sort of thing should not be necessary. And I guess a better way to solve this specific example would have been to pass a completely new instance of the same record (like below), protecting my variable from unintended changes by other code.

public void doComplexWork(List<MyObject__c> records) {
    fflib_ISObjectUnitOfWork uow = ClassFactory.newUOWInstance();
    for(MyObject__c record : records) {
        doSomeStuff(record, uow);
        optionallyDoSomeMoreStuff(record.clone(true, true, true, true), uow);
    }
    uow.commitWork();
}

//imagine this is in another class somewhere
private void doSomeStuff(MyObject__c record, fflib_ISObjectUnitOfWork uow) {
    record.CustomField__c = evaluateStuff(record); //evaluates to "1"
    uow.registerDirty(
        record,
        new List<SObjectField>{ MyObject__c.CustomField__c }
    );
}

The main lesson here then: SObject instances are passed by value, but the value is a pointer to the same instance. Even when calling methods in famous libraries you didn’t write yourself 🙂

I’ve created a little Test class which helps to demonstrate clearly how different combinations of references and method arguments behave when using the UoW’s registerDirty method. Check it out here. It’s definitely not trying to suggest it behaves wrongly (even if the tests are written to fail). It just highlights that first-glance assumptions may often be wrong.

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
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback
DmlWork a.k.a. the UoW meets Triggers - Pragmatic Bear
1 year ago

[…] UoW and SObject Instances […]

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