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
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…
[…] UoW and SObject Instances […]