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
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…
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.
[…] 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 […]