From f2553b028a9f5cc01e9e5405bbff3153bf6ff20b Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Wed, 16 Mar 2022 14:09:17 +0000 Subject: [PATCH 1/4] Add overloaded method for register dirty with relationships --- .../main/classes/fflib_SObjectUnitOfWork.cls | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index 324265def06..579266d05b6 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -280,6 +280,37 @@ public virtual class fflib_SObjectUnitOfWork registerRelationship(record, relatedToParentField, relatedToParentRecord); } + /** + * Register newly created records to be inserted when commitWork is called, + * it also provides a reference to the single parent record instance (should also be registered as new separately) + * + * @param records newly created records of the same SObjectType to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + public void registerNew(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) + { + if (records.isEmpty()) return; + + // Only validate the first record, by definition of the method signature all records should be new and of the same type. + Schema.SObjectType sObjectType = records.get(0).getSObjectType(); + String sObjectName = sObjectType.getDescribe().getName(); + assertForNonEventSObjectType(sObjectName); + assertForSupportedSObjectType(m_newListByType, sObjectName); + + for (SObject record : records) + { + if (record.getSObjectType() != sObjectType) + throw new UnitOfWorkException('All records should be of the same SObjectType'); + if (record.Id != null) + throw new UnitOfWorkException('Only new records can be registered as new'); + + m_newListByType.get(sObjectName).add(record); + if (relatedToParentRecord != null && relatedToParentField != null) + registerRelationship(record, relatedToParentField, relatedToParentRecord); + } + } + /** * Register a relationship between two records that have yet to be inserted to the database. This information will be * used during the commitWork phase to make the references only when related records have been inserted to the database. @@ -395,7 +426,7 @@ public virtual class fflib_SObjectUnitOfWork * Register an existing record to be updated when commitWork is called, * you may also provide a reference to the parent record instance (should also be registered as new separately) * - * @param record A newly created SObject instance to be inserted during commitWork + * @param record An existing SObject instance to be updated during commitWork * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) **/ @@ -406,6 +437,37 @@ public virtual class fflib_SObjectUnitOfWork registerRelationship(record, relatedToParentField, relatedToParentRecord); } + /** + * Register existing records to be updated when commitWork is called, + * it provide a reference to the parent record instance (should also be registered as new separately) + * + * @param records Existing records to be updated during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + public void registerDirty(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) + { + if (records.isEmpty()) return; + + // Only validate the first record, by definition of the method signature all records should be new and of the same type. + Schema.SObjectType sObjectType = records.get(0).getSObjectType(); + String sObjectName = sObjectType.getDescribe().getName(); + assertForNonEventSObjectType(sObjectName); + assertForSupportedSObjectType(m_dirtyMapByType, sObjectName); + + for (SObject record : records) + { + if (record.getSObjectType() != sObjectType) + throw new UnitOfWorkException('All records should be of the same SObjectType'); + if (record.Id == null) + throw new UnitOfWorkException('New records cannot be registered as dirty'); + + m_dirtyMapByType.get(sObjectName).put(record.Id, record); + if (relatedToParentRecord != null && relatedToParentField != null) + registerRelationship(record, relatedToParentField, relatedToParentRecord); + } + } + /** * Register a list of existing records to be updated during the commitWork method * From f7aa2bbd3975ba10edbce0f809f0b41cac57a67b Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Wed, 6 Apr 2022 08:42:45 +0100 Subject: [PATCH 2/4] Avoid code duplication Routes other registerNew methods to the bulkified method overload. --- .../main/classes/fflib_SObjectUnitOfWork.cls | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index 579266d05b6..28feb2d451a 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -252,10 +252,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerNew(List records) { - for (SObject record : records) - { - registerNew(record, null, null); - } + registerNew(records, null, null); } /** @@ -268,16 +265,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) { - if (record.Id != null) - throw new UnitOfWorkException('Only new records can be registered as new'); - String sObjectType = record.getSObjectType().getDescribe().getName(); - - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_newListByType, sObjectType); - - m_newListByType.get(sObjectType).add(record); - if (relatedToParentRecord!=null && relatedToParentField!=null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); + registerNew(new List {record}, relatedToParentField, relatedToParentRecord); } /** From 43be47f47bfd3882c88b3a6670f6de84cd89ab8e Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Thu, 7 Apr 2022 11:21:58 +0100 Subject: [PATCH 3/4] Change methods into using bulk Add bulk by default to registerNew & registerDirty Increased unit-test coverage with using bulk Use Custom Labels for exception messages --- .../main/classes/fflib_ISObjectUnitOfWork.cls | 18 ++ .../main/classes/fflib_SObjectUnitOfWork.cls | 291 +++++++++++------- ...b-Apex-Common-CustomLabels.labels-meta.xml | 77 +++++ .../classes/fflib_SObjectUnitOfWorkTest.cls | 117 ++++++- .../test/classes/mocks/fflib_SObjectMocks.cls | 10 + 5 files changed, 384 insertions(+), 129 deletions(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls index a18e1256f40..5e89300a4fb 100644 --- a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls @@ -50,6 +50,15 @@ public interface fflib_ISObjectUnitOfWork * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) **/ void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord); + /** + * Register newly created records to be inserted when commitWork is called, + * it also provides a reference to the single parent record instance (should also be registered as new separately) + * + * @param records newly created records of the same SObjectType to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + void registerNew(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord); /** * Register a relationship between two records that have yet to be inserted to the database. This information will be * used during the commitWork phase to make the references only when related records have been inserted to the database. @@ -59,6 +68,15 @@ public interface fflib_ISObjectUnitOfWork * @param relatedTo A SObject instance (yet to be committed to the database) */ void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + void registerRelationship(List records, Schema.SObjectField relatedToField, SObject relatedTo); /** * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted * to the database. This information will be diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index 28feb2d451a..fd3c4d387cd 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -285,37 +285,49 @@ public virtual class fflib_SObjectUnitOfWork String sObjectName = sObjectType.getDescribe().getName(); assertForNonEventSObjectType(sObjectName); assertForSupportedSObjectType(m_newListByType, sObjectName); + assertForNewRecordsWithSameSObjectType(records, sObjectType); - for (SObject record : records) - { - if (record.getSObjectType() != sObjectType) - throw new UnitOfWorkException('All records should be of the same SObjectType'); - if (record.Id != null) - throw new UnitOfWorkException('Only new records can be registered as new'); + m_newListByType.get(sObjectName).addAll(records); - m_newListByType.get(sObjectName).add(record); - if (relatedToParentRecord != null && relatedToParentField != null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); - } + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(records, relatedToParentField, relatedToParentRecord); } - /** - * Register a relationship between two records that have yet to be inserted to the database. This information will be - * used during the commitWork phase to make the references only when related records have been inserted to the database. - * - * @param record An existing or newly created record - * @param relatedToField A SObjectField reference to the lookup field that relates the two records together - * @param relatedTo A SObject instance (yet to be committed to the database) - */ - public void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) - { - String sObjectType = record.getSObjectType().getDescribe().getName(); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param record An existing or newly created record + * @param relatedToField A SObjectField reference to the lookup field that relates the two records together + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + public void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) + { + registerRelationship(new List {record}, relatedToField, relatedTo); + } - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_newListByType, sObjectType); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + public void registerRelationship(List records, Schema.SObjectField relatedToField, SObject relatedTo) + { + if (records.isEmpty()) return; - m_relationships.get(sObjectType).add(record, relatedToField, relatedTo); - } + // Only validate the first record, all records should of the same type. + Schema.SObjectType sObjectType = records.get(0).getSObjectType(); + String sObjectName = sObjectType.getDescribe().getName(); + assertForNonEventSObjectType(sObjectName); + assertForSupportedSObjectType(m_newListByType, sObjectName); + assertForSameSObjectType(records, sObjectType); + + m_relationships.get(sObjectName).add(records, relatedToField, relatedTo); + } /** * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted @@ -348,7 +360,7 @@ public virtual class fflib_SObjectUnitOfWork // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker String sObjectType = record.getSObjectType().getDescribe().getName(); if(!m_newListByType.containsKey(sObjectType)) - throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); + throw new UnitOfWorkException(String.format(Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, new String[] { sObjectType })); m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId); } @@ -362,52 +374,66 @@ public virtual class fflib_SObjectUnitOfWork registerDirty(record, new List()); } - /** - * 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 records, List 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 dirtyFields) - { - if (record.Id == null) - throw new UnitOfWorkException('New records cannot be registered as dirty'); - String sObjectType = record.getSObjectType().getDescribe().getName(); + public void registerDirty(SObject record, List dirtyFields) + { + registerDirty(new List {record}, dirtyFields); + } - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_dirtyMapByType, sObjectType); + /** + * 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(List records, List dirtyFields) + { + if (records.isEmpty()) return; - // 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); + // Only validate the first record, all records should be existing and of the same type. + Schema.SObjectType sObjectType = records.get(0).getSObjectType(); + String sObjectName = sObjectType.getDescribe().getName(); - for (SObjectField dirtyField : dirtyFields) { - registeredRecord.put(dirtyField, record.get(dirtyField)); - } + assertForNonEventSObjectType(sObjectName); + assertForSupportedSObjectType(m_dirtyMapByType, sObjectName); + assertForExistingRecordsWithSameSObjectType(records, sObjectType); - m_dirtyMapByType.get(sObjectType).put(record.Id, registeredRecord); - } + // Register the record as dirty, if no dirty fields or all records are not registered as dirty + if (dirtyFields.isEmpty() + || + !m_dirtyMapByType.get(sObjectName).keySet().containsAll(new Map(records).keySet())) + { + m_dirtyMapByType.get(sObjectName).putAll(records); + } + else + { + // Check records one by one and resolve dirty fields. + for (SObject record : records) + { + // Register the record as dirty, if record isn't registered already + if (!m_dirtyMapByType.get(sObjectName).containsKey(record.Id)) + { + m_dirtyMapByType.get(sObjectName).put(record.Id, record); + } + else + { + // Update the registered record's fields + SObject registeredRecord = m_dirtyMapByType.get(sObjectName).get(record.Id); + + for (SObjectField dirtyField : dirtyFields) + { + registeredRecord.put(dirtyField, record.get(dirtyField)); + } + + m_dirtyMapByType.get(sObjectName).put(record.Id, registeredRecord); + } + } + } } /** @@ -420,9 +446,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDirty(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) { - registerDirty(record); - if (relatedToParentRecord!=null && relatedToParentField!=null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); + registerDirty(new List {record}, relatedToParentField, relatedToParentRecord); } /** @@ -442,18 +466,13 @@ public virtual class fflib_SObjectUnitOfWork String sObjectName = sObjectType.getDescribe().getName(); assertForNonEventSObjectType(sObjectName); assertForSupportedSObjectType(m_dirtyMapByType, sObjectName); + assertForExistingRecordsWithSameSObjectType(records, sObjectType); - for (SObject record : records) - { - if (record.getSObjectType() != sObjectType) - throw new UnitOfWorkException('All records should be of the same SObjectType'); - if (record.Id == null) - throw new UnitOfWorkException('New records cannot be registered as dirty'); + m_dirtyMapByType.get(sObjectName).putAll(records); - m_dirtyMapByType.get(sObjectName).put(record.Id, record); - if (relatedToParentRecord != null && relatedToParentField != null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); - } + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(records, relatedToParentField, relatedToParentRecord); } /** @@ -463,10 +482,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDirty(List records) { - for (SObject record : records) - { - this.registerDirty(record); - } + registerDirty(records, new List()); } /** @@ -476,14 +492,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerUpsert(SObject record) { - if (record.Id == null) - { - registerNew(record, null, null); - } - else - { - registerDirty(record, new List()); - } + registerUpsert(new List {record}); } /** @@ -493,10 +502,15 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerUpsert(List records) { - for (SObject record : records) - { - this.registerUpsert(record); - } + List forInsert = new List(); + List forUpdate = new List(); + for (SObject record : records) + { + if (record.Id == null) forInsert.add(record); + else forUpdate.add(record); + } + if (!forInsert.isEmpty()) registerNew(forInsert); + if (!forUpdate.isEmpty()) registerDirty(forUpdate); } /** @@ -507,7 +521,7 @@ public virtual class fflib_SObjectUnitOfWork public void registerDeleted(SObject record) { if (record.Id == null) - throw new UnitOfWorkException('New records cannot be registered for deletion'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_deletion); String sObjectType = record.getSObjectType().getDescribe().getName(); assertForNonEventSObjectType(sObjectType); @@ -784,9 +798,7 @@ public virtual class fflib_SObjectUnitOfWork if (sObjectType.length() > 3 && sObjectType.right(3) == '__e') { throw new UnitOfWorkException( - String.format( - 'SObject type {0} must use registerPublishBeforeTransaction or ' + - 'registerPublishAfterTransaction methods to be used within this unit of work', + String.format(Label.fflib_SObjectUnitOfWork_event_SObjectType_must_use_publish, new List { sObjectType } ) ); @@ -799,14 +811,47 @@ public virtual class fflib_SObjectUnitOfWork if (sObjectType.length() > 3 && sObjectType.right(3) != '__e') { throw new UnitOfWorkException( - String.format( - 'SObject type {0} is invalid for publishing within this unit of work', + String.format(Label.fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish, new List {sObjectType} ) ); } } + private void assertForExistingRecordsWithSameSObjectType(List records, Schema.SObjectType sObjectType) + { + for (SObject record : records) + { + if (record.Id == null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_update); + + if (record.getSObjectType() != sObjectType) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_require_same_SObjectType); + } + } + + private void assertForNewRecordsWithSameSObjectType(List records, Schema.SObjectType sObjectType) + { + for (SObject record : records) + { + if (record.Id != null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_only_new_records_supported); + + if (record.getSObjectType() != sObjectType) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_require_same_SObjectType); + } + } + + private void assertForSameSObjectType(List records, Schema.SObjectType sObjectType) + { + for (SObject record : records) + { + if (record.getSObjectType() == sObjectType) continue; + + throw new UnitOfWorkException('All records should be of the same SObjectType'); + } + } + @TestVisible private void assertForSupportedSObjectType(Map theMap, String sObjectType) { @@ -814,7 +859,7 @@ public virtual class fflib_SObjectUnitOfWork { throw new UnitOfWorkException( String.format( - 'SObject type {0} is not supported by this unit of work', + Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, new List { sObjectType } ) ); @@ -839,12 +884,12 @@ public virtual class fflib_SObjectUnitOfWork public void add(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId) { if (relatedToField == null) { - throw new UnitOfWorkException('Invalid argument: relatedToField.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_relatedToField_null); } String relationshipName = relatedToField.getDescribe().getRelationshipName(); if (String.isBlank(relationshipName)) { - throw new UnitOfWorkException('Invalid argument: relatedToField. Field supplied is not a relationship field.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_relatedToField_invalid); } List relatedObjects = relatedToField.getDescribe().getReferenceTo(); @@ -855,32 +900,40 @@ public virtual class fflib_SObjectUnitOfWork Boolean externalIdFieldIsValid = externalIdField.getDescribe().isExternalId(); if (!relatedHasExternalIdField) { - throw new UnitOfWorkException('Invalid argument: externalIdField. Field supplied is not a known field on the target sObject.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_externalIdField_unknown); } if (!externalIdFieldIsValid) { - throw new UnitOfWorkException('Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_externalIdField_invalid); } - RelationshipByExternalId relationship = new RelationshipByExternalId(); - relationship.Record = record; - relationship.RelatedToField = relatedToField; - relationship.RelatedTo = relatedObject; - relationship.RelationshipName = relationshipName; - relationship.ExternalIdField = externalIdField; - relationship.ExternalId = externalId; - m_relationships.add(relationship); + RelationshipByExternalId relationship = new RelationshipByExternalId(); + relationship.Record = record; + relationship.RelatedToField = relatedToField; + relationship.RelatedTo = relatedObject; + relationship.RelationshipName = relationshipName; + relationship.ExternalIdField = externalIdField; + relationship.ExternalId = externalId; + m_relationships.add(relationship); } - public void add(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) - { - // Relationship to resolve - Relationship relationship = new Relationship(); - relationship.Record = record; - relationship.RelatedToField = relatedToField; - relationship.RelatedTo = relatedTo; - m_relationships.add(relationship); - } + public void add(List records, Schema.SObjectField relatedToField, SObject relatedTo) + { + for (SObject record : records) + { + add(record, relatedToField, relatedTo); + } + } + + public void add(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) + { + // Relationship to resolve + Relationship relationship = new Relationship(); + relationship.Record = record; + relationship.RelatedToField = relatedToField; + relationship.RelatedTo = relatedTo; + m_relationships.add(relationship); + } public void add(Messaging.SingleEmailMessage email, SObject relatedTo) { diff --git a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml index d4002b0a0d3..085345765ce 100644 --- a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml +++ b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml @@ -1,5 +1,82 @@ + + fflib_SObjectUnitOfWork_event_SObjectType_must_use_publish + en_US + false + Error when trying use non-publish method on Event SObjectTypes. + SObject type {0} must use registerPublishBeforeTransaction or registerPublishAfterTransaction methods to be used within this unit of work + + + fflib_SObjectUnitOfWork_invalid_argument_externalIdField_invalid + en_US + false + Error when the provided externalIdField is not an external Id. + Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier. + + + fflib_SObjectUnitOfWork_invalid_argument_externalIdField_unknown + en_US + false + Error when the provided externalIdField is not available on the SObjectType. + Invalid argument: externalIdField. Field supplied is not a known field on the target sObject. + + + fflib_SObjectUnitOfWork_invalid_argument_relatedToField_null + en_US + false + Error when relatedToField argument is null. + Invalid argument: relatedToField. + + + fflib_SObjectUnitOfWork_invalid_argument_relatedToField_invalid + en_US + false + Error when relatedToField argument is not a relationship field. + Invalid argument: relatedToField. Field supplied is not a relationship field. + + + fflib_SObjectUnitOfWork_new_records_not_supported_for_deletion + en_US + false + Error when trying to add new records for deletion. + New records cannot be registered for deletion + + + fflib_SObjectUnitOfWork_new_records_not_supported_for_update + en_US + false + Error when trying to add new records as dirty. + New records cannot be registered as dirty + + + fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish + en_US + false + Error when trying use publish method with a non-Event SObjectType. + SObject type {0} is invalid for publishing within this unit of work + + + fflib_SObjectUnitOfWork_not_supported_SObjectType + en_US + false + Error when adding records of a not supported SObjectType to the UnitOfWork. + SObject type {0} is not supported by this unit of work + + + fflib_SObjectUnitOfWork_only_new_records_supported + en_US + false + Error when adding existing records as new. + Only new records can be registered + + + fflib_SObjectUnitOfWork_require_same_SObjectType + en_US + false + Error when adding records of different SObjectTypes. + All records should be of the same SObjectType + fflib_QueryFactory_crossobject_fieldsets_not_allowed_error en_US diff --git a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls index 06274a9f38f..8de465e7c7c 100644 --- a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls +++ b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls @@ -83,9 +83,8 @@ private with sharing class fflib_SObjectUnitOfWorkTest catch (Exception e) { exceptionThrown = true; - System.assertEquals( - 'Only new records can be registered as new', - e.getMessage(), + System.assert( + e.getMessage().contains(Label.fflib_SObjectUnitOfWork_only_new_records_supported), 'Incorrect exception message thrown' ); } @@ -94,6 +93,53 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assert(exceptionThrown); } + @IsTest + private static void testRegisterNew_Bulk() + { + List records = new List + { + new Opportunity(Name = 'A'), + new Opportunity(Name = 'B'), + new Opportunity(Name = 'C') + }; + + Test.startTest(); + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerNew(records); + uow.commitWork(); + Test.stopTest(); + + System.assertEquals(3, mockDML.recordsForInsert.size()); + } + + @IsTest + private static void testRegisterNew_Bulk_ThrowExceptionOnMixedSObjectTypes() + { + List records = new List + { + new Opportunity(Name = 'A'), + new Opportunity(Name = 'B'), + new Product2(Name = 'A') + }; + + Boolean exceptionThrown = false; + try + { + Test.startTest(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + uow.registerNew(records); + uow.commitWork(); + Test.stopTest(); + } + catch (fflib_SObjectUnitOfWork.UnitOfWorkException e) + { + exceptionThrown = true; + System.assert(e.getMessage().contains(Label.fflib_SObjectUnitOfWork_require_same_SObjectType)); + } + System.assert(exceptionThrown, 'Expected an exception but did not occur'); + } + @IsTest private static void testRegisterDirty_ThrowExceptionOnNewRecord() { @@ -111,7 +157,7 @@ private with sharing class fflib_SObjectUnitOfWorkTest { exceptionThrown = true; System.assertEquals( - 'New records cannot be registered as dirty', + Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_update, e.getMessage(), 'Incorrect exception message thrown' ); @@ -212,15 +258,21 @@ private with sharing class fflib_SObjectUnitOfWorkTest { Boolean exceptionThrown = false; fflib_SObjectUnitOfWork unitOfWork = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + final String customObjectName = 'CustomObject__c'; try { - unitOfWork.assertForEventSObjectType('CustomObject__c'); + unitOfWork.assertForEventSObjectType(customObjectName); } catch (Exception e) { exceptionThrown = true; - System.assert( - e.getMessage().contains('invalid for publishing'), + final String errorMessage = + String.format( + Label.fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish, + new List {customObjectName}); + System.assertEquals( + errorMessage, + e.getMessage(), 'Incorrect exception message thrown' ); } @@ -240,8 +292,13 @@ private with sharing class fflib_SObjectUnitOfWorkTest catch (Exception e) { exceptionThrown = true; - System.assert( - e.getMessage().contains('not supported by this unit of work'), + final String errorMessage = + String.format( + Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, + new List {'Account'}); + System.assertEquals( + errorMessage, + e.getMessage(), 'Incorrect exception message thrown' ); } @@ -358,7 +415,7 @@ private with sharing class fflib_SObjectUnitOfWorkTest * - Correct events are fired when commitWork fails during DoWork processing * */ - @isTest + @IsTest private static void testDerivedUnitOfWork_CommitDoWorkFail() { // Insert Opportunities with UnitOfWork @@ -460,6 +517,46 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assertEquals(amountUpdate.Amount, mockDML.recordsForUpdate.get(0).get(Schema.Opportunity.Amount)); } + @IsTest + private static void testRegisterDirtyRecordsWithRelationShip() + { + final Id parentId = fflib_IDGenerator.generate(Account.SObjectType); + final Id opportunityIdA = fflib_IDGenerator.generate(Opportunity.SObjectType); + final Id opportunityIdB = fflib_IDGenerator.generate(Opportunity.SObjectType); + final Account parentRecord = new Account(Id = parentId); + + Opportunity opportunityA = new Opportunity(Id = opportunityIdA); + Opportunity opportunityB = new Opportunity(Id = opportunityIdB); + + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerDirty( + new List{ opportunityA, opportunityB }, + Schema.Opportunity.AccountId, + parentRecord); + uow.commitWork(); + + System.assertEquals(2, mockDML.recordsForUpdate.size()); + System.assert( + new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => opportunityIdA, + Opportunity.AccountId => parentId + }, + new Map + { + Opportunity.Id => opportunityIdB, + Opportunity.AccountId => parentId + } + } + ) + .matches(mockDML.recordsForUpdate), + 'Records not registered with the correct values' + ); + } + /** * Try registering a single field as dirty on multiple records. * 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..ee6fa18b99a 100644 --- a/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls +++ b/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls @@ -102,11 +102,21 @@ public class fflib_SObjectMocks mocks.mockVoidMethod(this, 'registerNew', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {record, relatedToParentField, relatedToParentRecord}); } + public void registerNew(List records, SObjectField relatedToParentField, SObject relatedToParentRecord) + { + mocks.mockVoidMethod(this, 'registerNew', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {records, relatedToParentField, relatedToParentRecord}); + } + public void registerRelationship(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) { mocks.mockVoidMethod(this, 'registerRelationship', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {record, relatedToField, relatedTo}); } + public void registerRelationship(List records, SObjectField relatedToField, SObject relatedTo) + { + mocks.mockVoidMethod(this, 'registerRelationship', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {records, relatedToField, relatedTo}); + } + public void registerRelationship(Messaging.SingleEmailMessage email, SObject relatedTo) { mocks.mockVoidMethod(this, 'registerRelationship', new List {Messaging.SingleEmailMessage.class, SObject.class}, new List {email, relatedTo}); From 4d28a2975fa35f83df2ba9126878dd9257def57a Mon Sep 17 00:00:00 2001 From: wimvelzeboer Date: Thu, 7 Apr 2022 13:17:53 +0100 Subject: [PATCH 4/4] Bulk method can accept different SObjectTypes at once --- .../main/classes/fflib_SObjectUnitOfWork.cls | 376 ++++++++++++------ ...b-Apex-Common-CustomLabels.labels-meta.xml | 13 +- .../classes/fflib_SObjectUnitOfWorkTest.cls | 29 +- 3 files changed, 250 insertions(+), 168 deletions(-) diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index fd3c4d387cd..e4e5f5e0dce 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -216,10 +216,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerEmptyRecycleBin(SObject record) { - String sObjectType = record.getSObjectType().getDescribe().getName(); - assertForSupportedSObjectType(m_emptyRecycleBinMapByType, sObjectType); - - m_emptyRecycleBinMapByType.get(sObjectType).put(record.Id, record); + registerEmptyRecycleBin(new List {record}); } /** @@ -229,9 +226,13 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerEmptyRecycleBin(List records) { - for (SObject record : records) + if (records.isEmpty()) return; + + Map> sortedRecords = + sortAndValidateForExistingNonEvent(m_emptyRecycleBinMapByType, records); + for (String sObjectName : sortedRecords.keySet()) { - registerEmptyRecycleBin(record); + registerEmptyRecycleBin(sObjectName, sortedRecords.get(sObjectName)); } } @@ -272,7 +273,7 @@ public virtual class fflib_SObjectUnitOfWork * Register newly created records to be inserted when commitWork is called, * it also provides a reference to the single parent record instance (should also be registered as new separately) * - * @param records newly created records of the same SObjectType to be inserted during commitWork + * @param records newly created records to be inserted during commitWork * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) **/ @@ -280,18 +281,11 @@ public virtual class fflib_SObjectUnitOfWork { if (records.isEmpty()) return; - // Only validate the first record, by definition of the method signature all records should be new and of the same type. - Schema.SObjectType sObjectType = records.get(0).getSObjectType(); - String sObjectName = sObjectType.getDescribe().getName(); - assertForNonEventSObjectType(sObjectName); - assertForSupportedSObjectType(m_newListByType, sObjectName); - assertForNewRecordsWithSameSObjectType(records, sObjectType); - - m_newListByType.get(sObjectName).addAll(records); - - if (relatedToParentRecord == null || relatedToParentField == null) return; - - registerRelationship(records, relatedToParentField, relatedToParentRecord); + Map> sortedRecords = sortAndValidateForNewNonEvent(m_newListByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerNew(sObjectName, sortedRecords.get(sObjectName), relatedToParentField, relatedToParentRecord); + } } /** @@ -319,14 +313,12 @@ public virtual class fflib_SObjectUnitOfWork { if (records.isEmpty()) return; - // Only validate the first record, all records should of the same type. - Schema.SObjectType sObjectType = records.get(0).getSObjectType(); - String sObjectName = sObjectType.getDescribe().getName(); - assertForNonEventSObjectType(sObjectName); - assertForSupportedSObjectType(m_newListByType, sObjectName); - assertForSameSObjectType(records, sObjectType); + Map> sortedRecords = sortAndValidateForNonEvent(m_newListByType, records); - m_relationships.get(sObjectName).add(records, relatedToField, relatedTo); + for (String sObjectName : sortedRecords.keySet()) + { + registerRelationship(sObjectName, sortedRecords.get(sObjectName), relatedToField, relatedTo); + } } /** @@ -357,11 +349,12 @@ public virtual class fflib_SObjectUnitOfWork */ public void registerRelationship(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId) { - // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker - String sObjectType = record.getSObjectType().getDescribe().getName(); - if(!m_newListByType.containsKey(sObjectType)) - throw new UnitOfWorkException(String.format(Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, new String[] { sObjectType })); - m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId); + // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker + String sObjectType = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(m_newListByType, sObjectType); + + m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId); } /** @@ -388,53 +381,19 @@ public virtual class fflib_SObjectUnitOfWork /** * 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 records SObjects to register as dirty * @param dirtyFields A list of modified fields */ public void registerDirty(List records, List dirtyFields) { if (records.isEmpty()) return; - // Only validate the first record, all records should be existing and of the same type. - Schema.SObjectType sObjectType = records.get(0).getSObjectType(); - String sObjectName = sObjectType.getDescribe().getName(); - - assertForNonEventSObjectType(sObjectName); - assertForSupportedSObjectType(m_dirtyMapByType, sObjectName); - assertForExistingRecordsWithSameSObjectType(records, sObjectType); - - // Register the record as dirty, if no dirty fields or all records are not registered as dirty - if (dirtyFields.isEmpty() - || - !m_dirtyMapByType.get(sObjectName).keySet().containsAll(new Map(records).keySet())) - { - m_dirtyMapByType.get(sObjectName).putAll(records); - } - else + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_dirtyMapByType, records); + for (String sObjectName : sortedRecords.keySet()) { - // Check records one by one and resolve dirty fields. - for (SObject record : records) - { - // Register the record as dirty, if record isn't registered already - if (!m_dirtyMapByType.get(sObjectName).containsKey(record.Id)) - { - m_dirtyMapByType.get(sObjectName).put(record.Id, record); - } - else - { - // Update the registered record's fields - SObject registeredRecord = m_dirtyMapByType.get(sObjectName).get(record.Id); - - for (SObjectField dirtyField : dirtyFields) - { - registeredRecord.put(dirtyField, record.get(dirtyField)); - } - - m_dirtyMapByType.get(sObjectName).put(record.Id, registeredRecord); - } - } + registerDirty(sObjectName, sortedRecords.get(sObjectName), dirtyFields); } - } + } /** * Register an existing record to be updated when commitWork is called, @@ -461,18 +420,11 @@ public virtual class fflib_SObjectUnitOfWork { if (records.isEmpty()) return; - // Only validate the first record, by definition of the method signature all records should be new and of the same type. - Schema.SObjectType sObjectType = records.get(0).getSObjectType(); - String sObjectName = sObjectType.getDescribe().getName(); - assertForNonEventSObjectType(sObjectName); - assertForSupportedSObjectType(m_dirtyMapByType, sObjectName); - assertForExistingRecordsWithSameSObjectType(records, sObjectType); - - m_dirtyMapByType.get(sObjectName).putAll(records); - - if (relatedToParentRecord == null || relatedToParentField == null) return; - - registerRelationship(records, relatedToParentField, relatedToParentRecord); + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_dirtyMapByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerDirty(sObjectName, sortedRecords.get(sObjectName), relatedToParentField, relatedToParentRecord); + } } /** @@ -520,14 +472,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDeleted(SObject record) { - if (record.Id == null) - throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_deletion); - String sObjectType = record.getSObjectType().getDescribe().getName(); - - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_deletedMapByType, sObjectType); - - m_deletedMapByType.get(sObjectType).put(record.Id, record); + registerDeleted(new List {record}); } /** @@ -537,10 +482,13 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDeleted(List records) { - for (SObject record : records) - { - this.registerDeleted(record); - } + if (records.isEmpty()) return; + + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_deletedMapByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerDeleted(sObjectName, sortedRecords.get(sObjectName)); + } } /** @@ -561,8 +509,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerPermanentlyDeleted(SObject record) { - this.registerEmptyRecycleBin(record); - this.registerDeleted(record); + registerPermanentlyDeleted(new List {record}); } /** @@ -671,6 +618,129 @@ public virtual class fflib_SObjectUnitOfWork } } + /** + * Adds the records to the delete queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as deleted + */ + protected void registerDeleted(String sObjectName, List records) + { + m_deletedMapByType.get(sObjectName).putAll(records); + } + + /** + * Adds the records to the dirty and relationship queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as dirty + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + */ + protected void registerDirty( + String sObjectName, + List records, + Schema.SObjectField relatedToParentField, + SObject relatedToParentRecord) + { + m_dirtyMapByType.get(sObjectName).putAll(records); + + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(sObjectName, records, relatedToParentField, relatedToParentRecord); + } + + /** + * Adds the records to the dirty queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as dirty + * @param dirtyFields A list of modified fields + */ + protected void registerDirty(String sObjectName, List records, List dirtyFields) + { + // Register the record as dirty, if no dirty fields or all records are not registered as dirty + if (dirtyFields.isEmpty() + || + !m_dirtyMapByType.get(sObjectName).keySet().containsAll(new Map(records).keySet())) + { + m_dirtyMapByType.get(sObjectName).putAll(records); + } + else + { + // Check records one by one and resolve dirty fields. + for (SObject record : records) + { + // Register the record as dirty, if record isn't registered already + if (!m_dirtyMapByType.get(sObjectName).containsKey(record.Id)) + { + m_dirtyMapByType.get(sObjectName).put(record.Id, record); + } + else + { + // Update the registered record's fields + SObject registeredRecord = m_dirtyMapByType.get(sObjectName).get(record.Id); + + for (SObjectField dirtyField : dirtyFields) + { + registeredRecord.put(dirtyField, record.get(dirtyField)); + } + + m_dirtyMapByType.get(sObjectName).put(record.Id, registeredRecord); + } + } + } + } + + /** + * Adds the records to the emptyRecycleBin queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Records to register as emptyRecycleBin + */ + protected void registerEmptyRecycleBin(String sObjectName, List records) + { + m_emptyRecycleBinMapByType.get(sObjectName).putAll(records); + } + + /** + * Adds the records to the insert queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Records to register as new, all of the given SObjectType + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + */ + protected void registerNew( + String sObjectName, + List records, + Schema.SObjectField relatedToParentField, + SObject relatedToParentRecord) + { + m_newListByType.get(sObjectName).addAll(records); + + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(sObjectName, records, relatedToParentField, relatedToParentRecord); + } + + /** + * Adds the records to the relationship queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + protected void registerRelationship( + String sObjectName, + List records, + Schema.SObjectField relatedToField, + SObject relatedTo) + { + m_relationships.get(sObjectName).add(records, relatedToField, relatedTo); + } + private void doCommitWork() { onCommitWorkStarting(); @@ -818,65 +888,111 @@ public virtual class fflib_SObjectUnitOfWork } } - private void assertForExistingRecordsWithSameSObjectType(List records, Schema.SObjectType sObjectType) + private void assertForExistingRecord(SObject record) { - for (SObject record : records) - { - if (record.Id == null) - throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_update); + if (record.Id == null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported); + } + + private void assertForNewRecord(SObject record) + { + if (record.Id != null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_only_new_records_supported); + } - if (record.getSObjectType() != sObjectType) - throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_require_same_SObjectType); + @TestVisible + private void assertForSupportedSObjectType(Map theMap, String sObjectType) + { + if (!theMap.containsKey(sObjectType)) + { + throw new UnitOfWorkException( + String.format( + Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, + new List {sObjectType} + ) + ); } } - private void assertForNewRecordsWithSameSObjectType(List records, Schema.SObjectType sObjectType) + private Map> sortAndValidateForExistingNonEvent(Map theMap, List records) { + Map> result = new Map>(); for (SObject record : records) { - if (record.Id != null) - throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_only_new_records_supported); + String sObjectName = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(theMap, sObjectName); + assertForExistingRecord(record); + assertForNonEventSObjectType(sObjectName); - if (record.getSObjectType() != sObjectType) - throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_require_same_SObjectType); + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } } + return result; } - private void assertForSameSObjectType(List records, Schema.SObjectType sObjectType) + private Map> sortAndValidateForNewNonEvent(Map theMap, List records) { + Map> result = new Map>(); for (SObject record : records) { - if (record.getSObjectType() == sObjectType) continue; + String sObjectName = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(theMap, sObjectName); + assertForNewRecord(record); + assertForNonEventSObjectType(sObjectName); - throw new UnitOfWorkException('All records should be of the same SObjectType'); + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } } + return result; } - @TestVisible - private void assertForSupportedSObjectType(Map theMap, String sObjectType) + private Map> sortAndValidateForNonEvent(Map theMap, List records) { - if (!theMap.containsKey(sObjectType)) + Map> result = new Map>(); + for (SObject record : records) { - throw new UnitOfWorkException( - String.format( - Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, - new List { sObjectType } - ) - ); + String sObjectName = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(theMap, sObjectName); + assertForNonEventSObjectType(sObjectName); + + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } } + return result; } - private class Relationships - { - private List m_relationships = new List(); + private class Relationships + { + private List m_relationships = new List(); - public void resolve() - { - // Resolve relationships - for (IRelationship relationship : m_relationships) - { - //relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); - relationship.resolve(); + public void resolve() + { + // Resolve relationships + for (IRelationship relationship : m_relationships) + { + //relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); + relationship.resolve(); } } diff --git a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml index 085345765ce..a78c5ee7ef5 100644 --- a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml +++ b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml @@ -36,18 +36,11 @@ Invalid argument: relatedToField. Field supplied is not a relationship field. - fflib_SObjectUnitOfWork_new_records_not_supported_for_deletion + fflib_SObjectUnitOfWork_new_records_not_supported en_US false - Error when trying to add new records for deletion. - New records cannot be registered for deletion - - - fflib_SObjectUnitOfWork_new_records_not_supported_for_update - en_US - false - Error when trying to add new records as dirty. - New records cannot be registered as dirty + Error when trying to add new records as dirty or deleted. + New records cannot be registered fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish diff --git a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls index 8de465e7c7c..1c581ff626f 100644 --- a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls +++ b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls @@ -112,33 +112,6 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assertEquals(3, mockDML.recordsForInsert.size()); } - - @IsTest - private static void testRegisterNew_Bulk_ThrowExceptionOnMixedSObjectTypes() - { - List records = new List - { - new Opportunity(Name = 'A'), - new Opportunity(Name = 'B'), - new Product2(Name = 'A') - }; - - Boolean exceptionThrown = false; - try - { - Test.startTest(); - fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS); - uow.registerNew(records); - uow.commitWork(); - Test.stopTest(); - } - catch (fflib_SObjectUnitOfWork.UnitOfWorkException e) - { - exceptionThrown = true; - System.assert(e.getMessage().contains(Label.fflib_SObjectUnitOfWork_require_same_SObjectType)); - } - System.assert(exceptionThrown, 'Expected an exception but did not occur'); - } @IsTest private static void testRegisterDirty_ThrowExceptionOnNewRecord() @@ -157,7 +130,7 @@ private with sharing class fflib_SObjectUnitOfWorkTest { exceptionThrown = true; System.assertEquals( - Label.fflib_SObjectUnitOfWork_new_records_not_supported_for_update, + Label.fflib_SObjectUnitOfWork_new_records_not_supported, e.getMessage(), 'Incorrect exception message thrown' );