Skip to content

Commit 08a5984

Browse files
committed
feat: Add optional parameter for registerDirty to specify exactly what fields need to be updated
This allows a unit of work's uncommited changes to not be overwritten when multiple areas of code update the same record on the same unit of work. e.g. a stack of processes that update different fields on the same record while being passed a unit of work allowing for changes to be commited after http callouts Comes form Traction on Demand's John Rogers, has been sitting in our code base for a long time There is a breaking change compared to a previous versions of fflib, where an excpetion is thrown if the same record is registered dirty twice without fields being specified. This could be modified to overwrite the existing registered dirty record (which is the current behaviour) instead of throwing an exception.
1 parent 70002fb commit 08a5984

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed

fflib/src/classes/fflib_ISObjectUnitOfWork.cls

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ public interface fflib_ISObjectUnitOfWork
5959
* @param record An existing record
6060
**/
6161
void registerDirty(SObject record);
62+
/**
63+
* Register specific fields on record to be updated when work is commited
64+
*
65+
* If the record has previously been registered as dirty, the dirty fields on the record in this call will overwrite
66+
* the values of the previously registered dirty record
67+
*
68+
* @param record An existing record
69+
* @param dirtyFields The fields to update if record is already registered
70+
**/
71+
void registerDirty(SObject record, List<SObjectField> dirtyFields);
6272
/**
6373
* Register an existing record to be updated when commitWork is called,
6474
* you may also provide a reference to the parent record instance (should also be registered as new separatly)

fflib/src/classes/fflib_SObjectMocks.cls

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ public class fflib_SObjectMocks
8181
mocks.mockVoidMethod(this, 'registerDirty', new List<Type> {SObject.class}, new List<Object> {record});
8282
}
8383

84+
public void registerDirty(SObject record, List<SObjectField> dirtyFields)
85+
{
86+
mocks.mockVoidMethod(this, 'registerDirty', new List<Type> {
87+
SObject.class, System.Type.forName('List<SObjectField>')
88+
}, new List<Object> {
89+
record, dirtyFields
90+
});
91+
}
92+
8493
public void registerDirty(SObject record, Schema.sObjectField relatedToParentField, SObject relatedToParentRecord)
8594
{
8695
mocks.mockVoidMethod(this, 'registerDirty', new List<Type> {SObject.class}, new List<Object> {record});

fflib/src/classes/fflib_SObjectUnitOfWork.cls

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,41 @@ public virtual class fflib_SObjectUnitOfWork
247247
* @param record An existing record
248248
**/
249249
public void registerDirty(SObject record)
250+
{
251+
registerDirty(record, new List<SObjectField>());
252+
}
253+
254+
public void registerDirty(SObject record, List<SObjectField> dirtyFields)
250255
{
251256
if(record.Id == null)
252257
throw new UnitOfWorkException('New records cannot be registered as dirty');
253258
String sObjectType = record.getSObjectType().getDescribe().getName();
254259
if(!m_dirtyMapByType.containsKey(sObjectType))
255260
throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType }));
261+
262+
// If record isn't registered as dirty
263+
if (!m_dirtyMapByType.get(sObjectType).containsKey(record.Id))
264+
{
265+
// Register the record as dirty
266+
m_dirtyMapByType.get(sObjectType).put(record.Id, record);
267+
}
268+
else
269+
{
270+
// Update the registered record's fields
271+
SObject registeredRecord = m_dirtyMapByType.get(sObjectType).get(record.Id);
272+
273+
// If the caller has supplied a different instance of the same record with no list of updated fields
274+
if (dirtyFields.isEmpty() && registeredRecord !== record)
275+
{
276+
// Cannot determine what updates to make to the record (assuming updating nothing is incorrect)
277+
throw new UnitOfWorkException('Cannot determine what fields to update on record ' + record);
278+
}
279+
280+
for (SObjectField dirtyField : dirtyFields) {
281+
registeredRecord.put(dirtyField, record.get(dirtyField));
282+
}
283+
}
284+
256285
m_dirtyMapByType.get(sObjectType).put(record.Id, record);
257286
}
258287

fflib/src/classes/fflib_SObjectUnitOfWorkTest.cls

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,78 @@ private with sharing class fflib_SObjectUnitOfWorkTest
338338
, uow.getCommitWorkEventsFired(), new Set<Schema.SObjectType>(MY_SOBJECTS), uow.getRegisteredTypes());
339339
}
340340

341+
/**
342+
* Try registering two instances of the same record as dirty.
343+
*
344+
* Testing:
345+
*
346+
* - Exception is thrown stopping second registration
347+
*/
348+
@isTest
349+
private static void testRegisterDirty_DoubleException() {
350+
Opportunity opp = new Opportunity(Id = '00636000005loIj', Name = 'UpdateName');
351+
Opportunity opp2 = new Opportunity(Id = '00636000005loIj', Amount = 250);
352+
UnitOfWork uow = new UnitOfWork(MY_SOBJECTS);
353+
uow.registerDirty(opp);
354+
Boolean exceptionThrown = false;
355+
try {
356+
// Second registration would undo updates of first registration
357+
uow.registerDirty(opp2);
358+
} catch (UnitOfWork.UnitOfWorkException e) {
359+
System.assert(e.getMessage().contains(opp.Id));
360+
exceptionThrown = true;
361+
}
362+
System.assert(exceptionThrown);
363+
}
364+
365+
/**
366+
* Try registering a single field as dirty.
367+
*
368+
* Testing:
369+
*
370+
* - field is updated
371+
*/
372+
@isTest
373+
private static void testRegisterDirty_field() {
374+
Opportunity opp = new Opportunity(Name = 'test name', StageName = 'Open', CloseDate = System.today());
375+
insert opp;
376+
377+
Opportunity nameUpdate = new Opportunity(Id = opp.Id, Name = 'UpdateName');
378+
Opportunity amountUpdate = new Opportunity(Id = opp.Id, Amount = 250);
379+
UnitOfWork uow = new UnitOfWork(MY_SOBJECTS);
380+
uow.registerDirty(nameUpdate);
381+
uow.registerDirty(amountUpdate, Opportunity.Amount);
382+
uow.commitWork();
383+
384+
opp = [SELECT Name, Amount FROM Opportunity WHERE Id = :opp.Id];
385+
System.assertEquals(opp.Name, nameUpdate.Name);
386+
System.assertEquals(opp.Amount, amountUpdate.Amount);
387+
}
388+
389+
/**
390+
* Try registering a single unupdateable field as dirty in secure mode
391+
*
392+
* Testing:
393+
*
394+
* - exception is thrown
395+
*/
396+
@isTest
397+
private static void testRegisterDirty_field_secure() {
398+
Opportunity opp = new Opportunity(Name = 'test name', StageName = 'Open', CloseDate = System.today());
399+
insert opp;
400+
401+
UnitOfWork uow = new UnitOfWork(MY_SOBJECTS, new AccessConfig());
402+
uow.registerDirty(opp, Opportunity.Id); // Should throw exception because ID is not updatable
403+
404+
Boolean exceptionThrown = false;
405+
try {
406+
uow.commitWork();
407+
} catch (fflib_SecurityUtils.SecurityException e) {
408+
exceptionThrown = true;
409+
}
410+
System.assert(exceptionThrown);
411+
}
412+
341413
/**
342414
* Assert that actual events exactly match expected events (size, order and name)
343415
* and types match expected types

0 commit comments

Comments
 (0)