diff --git a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls index a18e1256f40..332241c4b4a 100644 --- a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls @@ -94,7 +94,9 @@ public interface fflib_ISObjectUnitOfWork * If the records are previously registered as dirty, the dirty fields on the records in this call will overwrite * the values of the previously registered dirty records * - * @param records A list of existing records + * Note: you should avoid mixing SObject types when registering dirty fields + * + * @param records A list of existing records * @param dirtyFields The fields to update if record is already registered **/ void registerDirty(List records, List dirtyFields); @@ -141,12 +143,30 @@ public interface fflib_ISObjectUnitOfWork * @param record An new or existing record **/ void registerUpsert(SObject record); + /** + * Register a new or existing record to be inserted/updated during the commitWork method, with a specific list of dirty fields to update / commit + * + * NOTE: If the record requires an insert, the dirtyFields list is ignored and ALL field values are included on the insert + * + * @param record A new or existing record + * @param dirtyFields A list of modified fields + **/ + void registerUpsert( SObject record, List dirtyFields ); /** * Register a list of mix of new and existing records to be upserted during the commitWork method * * @param records A list of mix of existing and new records **/ void registerUpsert(List records); + /** + * Register a list of mix of new and existing records to be inserted updated during the commitWork method, with a specific list of dirty fields to update / commit + * + * NOTE: If a record requires an insert, the dirtyFields list is ignored and ALL field values are included on the insert + * + * @param records A list of mix of new and existing records + * @param dirtyFields A list of modified fields + **/ + void registerUpsert( List records, List dirtyFields ); /** * Register an existing record to be deleted during the commitWork method * diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index e5b85b32dd0..065397c8fa0 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -349,8 +349,10 @@ public virtual class fflib_SObjectUnitOfWork } /** - * Registers the entire records as dirty or just only the dirty fields if the record was already registered + * Registers the stated fields on the records as dirty * + * Note: you should avoid mixing SObject types when registering dirty fields + * * @param records SObjects to register as dirty * @param dirtyFields A list of modified fields */ @@ -363,7 +365,7 @@ public virtual class fflib_SObjectUnitOfWork } /** - * Registers the entire record as dirty or just only the dirty fields if the record was already registered + * Registers the stated fields on the record as dirty * * @param record SObject to register as dirty * @param dirtyFields A list of modified fields @@ -395,9 +397,7 @@ public virtual class fflib_SObjectUnitOfWork // 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)); - } + copyFields( record, registeredRecord, dirtyFields ); m_dirtyMapByType.get(sObjectType).put(record.Id, registeredRecord); } @@ -448,6 +448,26 @@ public virtual class fflib_SObjectUnitOfWork } } + /** + * Register a new or existing record to be inserted/updated during the commitWork method, with a specific list of dirty fields to update / commit + * + * NOTE: If the record requires an insert, the dirtyFields list is ignored and ALL field values are included on the insert + * + * @param record A new or existing record + * @param dirtyFields A list of modified fields + **/ + public virtual void registerUpsert( SObject record, List dirtyFields ) + { + if ( record.Id == null ) + { + registerNew( record, null, null ); + } + else + { + registerDirty( record, dirtyFields ); + } + } + /** * Register a list of mix of new and existing records to be inserted updated during the commitWork method * @@ -461,6 +481,22 @@ public virtual class fflib_SObjectUnitOfWork } } + /** + * Register a list of mix of new and existing records to be inserted updated during the commitWork method, with a specific list of dirty fields to update / commit + * + * NOTE: If a record requires an insert, the dirtyFields list is ignored and ALL field values are included on the insert + * + * @param records A list of mix of new and existing records + * @param dirtyFields A list of modified fields + **/ + public virtual void registerUpsert( List records, List dirtyFields ) + { + for ( SObject record : records ) + { + this.registerUpsert( record, dirtyFields ); + } + } + /** * Register an existing record to be deleted during the commitWork method * @@ -783,6 +819,14 @@ public virtual class fflib_SObjectUnitOfWork } } + private void copyFields( Sobject fromRecord, Sobject toRecord, List fields ) + { + for ( SObjectField thisField : fields ) + { + toRecord.put( thisField, fromRecord.get( thisField ) ); + } + } + private class Relationships { private List m_relationships = new List(); diff --git a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls index cf3edd08ba6..d10705fa804 100644 --- a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls +++ b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls @@ -658,6 +658,124 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assertEquals(1, mockDML.recordsForInsert.size()); } + @isTest + private static void testRegisterUpsertForInsertWithDirtyFields() { + + Opportunity newOpportunity = new Opportunity( Name = 'New Opportunity', StageName = 'Closed', CloseDate = System.today() ); + + Test.startTest(); + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerUpsert( newOpportunity, new List{ Opportunity.Name, Opportunity.StageName } ); + uow.commitWork(); + Test.stopTest(); + + System.assertEquals( 0, mockDML.recordsForUpdate.size() ); + System.assertEquals( 1, mockDML.recordsForInsert.size() ); + + System.assert( + !new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => newOpportunity.Id, + Opportunity.Name => newOpportunity.Name, + Opportunity.StageName => newOpportunity.CloseDate + } + } + ) + .matches( mockDML.recordsForInsert ), + 'registerUpsert with dirty fields will ignore the dirty fields when sending them for insert' + ); + } + + @isTest + private static void testRegisterUpsertForUpdateWithDirtyFields() { + + Opportunity updatedOpportunity = new Opportunity( + Id = fflib_IDGenerator.generate(Schema.Opportunity.SObjectType), + Name = 'Existing Opportunity', + StageName = 'Closed', + CloseDate = System.today()); + + Test.startTest(); + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerUpsert( updatedOpportunity, new List{ Opportunity.Name, Opportunity.StageName } ); + uow.commitWork(); + Test.stopTest(); + + System.assertEquals( 1, mockDML.recordsForUpdate.size() ); + System.assertEquals( 0, mockDML.recordsForInsert.size() ); + + System.assert( + !new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => updatedOpportunity.Id, + Opportunity.Name => updatedOpportunity.Name, + Opportunity.StageName => '' + } + } + ) + .matches( mockDML.recordsForUpdate ), + 'registerUpsert with dirty fields will send the dirty fields when sending the record them for update' + ); + } + + @isTest + private static void testRegisterUpsertWithMultipleRecordsAndDirtyFields() { + + Opportunity updatedOpportunity = new Opportunity( + Id = fflib_IDGenerator.generate(Schema.Opportunity.SObjectType), + Name = 'Existing Opportunity', + StageName = 'Closed', + CloseDate = System.today()); + + Opportunity newOpportunity = new Opportunity( Name = 'New Opportunity', StageName = 'Closed', CloseDate = System.today() ); + + Test.startTest(); + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerUpsert( new List{ updatedOpportunity, newOpportunity }, new List{ Opportunity.Name, Opportunity.StageName } ); + uow.commitWork(); + Test.stopTest(); + + System.assertEquals(1, mockDML.recordsForUpdate.size()); + System.assertEquals(1, mockDML.recordsForInsert.size()); + + System.assert( + !new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => newOpportunity.Id, + Opportunity.Name => newOpportunity.Name, + Opportunity.StageName => newOpportunity.CloseDate + } + } + ) + .matches( mockDML.recordsForInsert ), + 'registerUpsert with dirty fields will ignore the dirty fields when sending them for insert' + ); + + System.assert( + !new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => updatedOpportunity.Id, + Opportunity.Name => updatedOpportunity.Name, + Opportunity.StageName => '' + } + } + ) + .matches( mockDML.recordsForUpdate ), + 'registerUpsert with dirty fields will send the dirty fields when sending the record them for update' + ); + } + /** * Assert that actual events exactly match expected events (size, order and name) * and types match expected types diff --git a/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls b/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls index b9e13ab12cb..fbc7b1a56e1 100644 --- a/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls +++ b/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls @@ -155,6 +155,11 @@ public class fflib_SObjectMocks mocks.mockVoidMethod(this, 'registerUpsert', new List {List.class}, new List {record}); } + public void registerUpsert(SObject record, List dirtyFields ) + { + mocks.mockVoidMethod(this, 'registerUpsert', new List {List.class}, new List {record, dirtyFields}); + } + public void registerEmptyRecycleBin(SObject record) { mocks.mockVoidMethod(this, 'registerEmptyRecycleBin', new List {List.class}, new List {record}); @@ -170,6 +175,11 @@ public class fflib_SObjectMocks mocks.mockVoidMethod(this, 'registerUpsert', new List {List.class}, new List {records}); } + public void registerUpsert(List records, List dirtyFields ) + { + mocks.mockVoidMethod(this, 'registerUpsert', new List {List.class}, new List {records, dirtyFields}); + } + public void registerDeleted(SObject record) { mocks.mockVoidMethod(this, 'registerDeleted', new List {SObject.class}, new List {record});