From fa7f132bf50ef34d84fbad3698cd601ca2c02898 Mon Sep 17 00:00:00 2001 From: Jon Davis Date: Wed, 12 Aug 2015 17:46:18 -0700 Subject: [PATCH 1/2] Add developer API to mirror triggerHandler behavior See Issue #196 and PR #208 for background --- rolluptool/src/classes/RollupService.cls | 238 +++++--- rolluptool/src/classes/RollupServiceTest3.cls | 533 ++++++++++++++++++ 2 files changed, 684 insertions(+), 87 deletions(-) diff --git a/rolluptool/src/classes/RollupService.cls b/rolluptool/src/classes/RollupService.cls index 5e5ae7e3..f00af46e 100644 --- a/rolluptool/src/classes/RollupService.cls +++ b/rolluptool/src/classes/RollupService.cls @@ -162,6 +162,26 @@ global with sharing class RollupService update masterRecords.values(); } + /** + * Developer API for the tool, only executes Rollup Summmaries with Calculation Mode set to Developer + * + * Automatically resolves child records to process via LREngine and lookups described in LookupRollupSummary__c + * also determines if based on the old trigger records if the rollup processing needs to occur + * + * @param existingRecords Existing records corresponding to the childRecords. + * @param newRecords Child records being modified + * @param sObjectType SObjectType of the existing/new records + * + * @usage rollup(Trigger.oldMap, Trigger.newMap, Account.SObjectType) + * + * @remark All SObjects (existing and new) must be of the same SObjectType + **/ + global static void rollup(Map existingRecords, Map newRecords, Schema.SObjectType sObjectType) + { + handleRollups(existingRecords, newRecords, sObjectType, new List { RollupSummaries.CalculationMode.Developer }); + } + + /** * Developer API for the tool, only executes Rollup Summmaries with Calculation Mode set to Developer * @@ -256,86 +276,9 @@ global with sharing class RollupService // Anything to rollup? List childRecords = Trigger.isDelete ? Trigger.old : Trigger.new; SObjectType childObjectType = childRecords[0].Id.getSObjectType(); - List lookups = describeRollups(childRecords[0].Id.getSObjectType()); - if(lookups.size()==0) - return; // Nothing to see here! :) - - // Has anything changed on the child records in respect to the fields referenced on the lookup definition? - if(Trigger.isUpdate) - { - // Master records to update - Set masterRecordIds = new Set(); - - // Set of field names from the child used in the rollup to search for changes on - Set fieldsToSearchForChanges = new Set(); - Set relationshipFields = new Set(); - for(LookupRollupSummary__c lookup : lookups) - { - fieldsToSearchForChanges.add(lookup.FieldToAggregate__c); - fieldsToSearchForChanges.add(lookup.RelationShipField__c); - if(lookup.RelationshipCriteriaFields__c!=null) - for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n')) - fieldsToSearchForChanges.add(criteriaField); - relationshipFields.add(lookup.RelationShipField__c); - } - - // Determine if a a field referenced on the lookup has changed and thus if the lookup itself needs recalculating - Set fieldsChanged = new Set(); - for(SObject childRecord : childRecords) - { - // Determine if any of the fields referenced on our selected rollups have changed on this record - for(String fieldToSearch : fieldsToSearchForChanges) - { - SObject oldChildRecord = Trigger.oldMap.get(childRecord.Id); - Object newValue = childRecord.get(fieldToSearch); - Object oldValue = oldChildRecord.get(fieldToSearch); - // Register this field as having changed? - if(newValue != oldValue) - fieldsChanged.add(fieldToSearch); - // Add both old and new value to master record Id list for relationship fields to ensure old and new parent master records are updated (re-parenting) - if(relationshipFields.contains(fieldToSearch)) - { - if(newValue!=null) - masterRecordIds.add((Id) newValue); - if(oldValue!=null) - masterRecordIds.add((Id) oldValue); - } - } - } - - // Build a revised list of lookups to process that includes only where fields used in the rollup have changed - List lookupsToProcess = new List(); - for(LookupRollupSummary__c lookup : lookups) - { - // Are any of the changed fields used by this lookup? - Boolean processLookup = false; - if(fieldsChanged.contains(lookup.FieldToAggregate__c) || - fieldsChanged.contains(lookup.RelationShipField__c)) - processLookup = true; - if(lookup.RelationshipCriteriaFields__c!=null) - for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n')) - if(fieldsChanged.contains(criteriaField)) - processLookup = true; - if(processLookup) - lookupsToProcess.add(lookup); - } - lookups = lookupsToProcess; - - // Rollup child records and update master records - if(lookupsToProcess.size()>0) - updateRecords(updateMasterRollupsTrigger(lookups, masterRecordIds), false, true); - return; - } - - // Rollup child records and update master records - Set masterRecordIds = new Set(); - for(SObject childRecord : childRecords) - for(LookupRollupSummary__c lookup : lookups) - if(childRecord.get(lookup.RelationShipField__c)!=null) - masterRecordIds.add((Id)childRecord.get(lookup.RelationShipField__c)); - updateRecords(updateMasterRollupsTrigger(lookups, masterRecordIds), false, true); + handleRollups(Trigger.oldMap, Trigger.newMap, childObjectType, new List { RollupSummaries.CalculationMode.Realtime, RollupSummaries.CalculationMode.Scheduled }); } - + /** * Method returns a QueryLocator that returns master records (as per the lookup definition) meeting the criteria expressed (if defined) **/ @@ -551,6 +494,126 @@ global with sharing class RollupService // Delete any old logs entries for master records that have now been updated successfully delete [select Id from LookupRollupSummaryLog__c where ParentId__c in :masterRecordsUpdatedId]; } + + /** + * Process rollups for specified modes + * + * @param childRecords List of childRecords to process rollups against + * @param existingRecords Map of existing records. Pass null if no existing records are available. + * @param calculationModes Modes to use to determine which rollups to evaluate and process + * + * @remark Will process both lists looking for insert/update/delete/undelete and execute rollups on the following conditions: + * 1) if in newRecords and not in existingRecords (insert and undelete) + * 2) if in existingRecords and not in newRecords (delete) + * 3) if in existingRecords and newRecords and rollup FieldToAggregate__c has changed (update) + * + **/ + private static void handleRollups(Map existingRecords, Map newRecords, Schema.SObjectType sObjectType, List calculationModes) + { + // make sure we have Maps to avoid conditional statements in loops below + if (existingRecords == null) { + existingRecords = new Map(); + } + if (newRecords == null) { + newRecords = new Map(); + } + + // Anything to process? + if (existingRecords.isEmpty() && newRecords.isEmpty()) { + return; + } + + List lookups = describeRollups(sObjectType, calculationModes); + if(lookups.isEmpty()) + return; // Nothing to see here! :) + + // Has anything changed on the child records in respect to the fields referenced on the lookup definition? + if(existingRecords != null && !existingRecords.isEmpty()) + { + // Master records to update + Set masterRecordIds = new Set(); + + // Set of field names from the child used in the rollup to search for changes on + Set fieldsToSearchForChanges = new Set(); + Set relationshipFields = new Set(); + for(LookupRollupSummary__c lookup : lookups) + { + fieldsToSearchForChanges.add(lookup.FieldToAggregate__c); + fieldsToSearchForChanges.add(lookup.RelationShipField__c); + if(lookup.RelationshipCriteriaFields__c!=null) + for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n')) + fieldsToSearchForChanges.add(criteriaField); + relationshipFields.add(lookup.RelationShipField__c); + } + + // merge all record Id's + Set mergedRecordIds = new Set(existingRecords.keySet()); + mergedRecordIds.addAll(newRecords.keySet()); + + // Determine if a a field referenced on the lookup has changed and thus if the lookup itself needs recalculating + Set fieldsChanged = new Set(); + for(Id recordId : mergedRecordIds) + { + // Determine if any of the fields referenced on our selected rollups have changed on this record + for(String fieldToSearch : fieldsToSearchForChanges) + { + // retrieve old and new records and values if they exist + SObject oldRecord = existingRecords.get(recordId); + Object oldValue = oldRecord == null ? null : oldRecord.get(fieldToSearch); + SObject newRecord = newRecords.get(recordId); + Object newValue = newRecord == null ? null : newRecord.get(fieldToSearch); + + // Register this field as having changed? + // if in old but not in new then its a delete and rollup should be processed + // if in new but not in old then its an insert and rollup should be processed + // if in both then its an update and field change detection should occur + if((oldRecord == null) || (newRecord == null) || (newValue != oldValue)) { + fieldsChanged.add(fieldToSearch); + } + + // Add both old and new value to master record Id list for relationship fields to ensure old and new parent master records are updated (re-parenting) + if(relationshipFields.contains(fieldToSearch)) + { + if(newValue!=null) + masterRecordIds.add((Id) newValue); + if(oldValue!=null) + masterRecordIds.add((Id) oldValue); + } + } + } + + // Build a revised list of lookups to process that includes only where fields used in the rollup have changed + List lookupsToProcess = new List(); + for(LookupRollupSummary__c lookup : lookups) + { + // Are any of the changed fields used by this lookup? + Boolean processLookup = false; + if(fieldsChanged.contains(lookup.FieldToAggregate__c) || + fieldsChanged.contains(lookup.RelationShipField__c)) + processLookup = true; + if(lookup.RelationshipCriteriaFields__c!=null) + for(String criteriaField : lookup.RelationshipCriteriaFields__c.split('\r\n')) + if(fieldsChanged.contains(criteriaField)) + processLookup = true; + if(processLookup) + lookupsToProcess.add(lookup); + } + lookups = lookupsToProcess; + + // Rollup child records and update master records + if(lookupsToProcess.size()>0) + updateRecords(updateMasterRollupsTrigger(lookups, masterRecordIds), false, true); + return; + } + + // Rollup child records and update master records + Set masterRecordIds = new Set(); + for(SObject childRecord : newRecords.values()) + for(LookupRollupSummary__c lookup : lookups) + if(childRecord.get(lookup.RelationShipField__c)!=null) + masterRecordIds.add((Id)childRecord.get(lookup.RelationShipField__c)); + updateRecords(updateMasterRollupsTrigger(lookups, masterRecordIds), false, true); + } /** * Method wraps the LREngine.rolup method, provides context via the lookups described in LookupRollupSummary__c @@ -562,17 +625,18 @@ global with sharing class RollupService private static List updateMasterRollupsTrigger(List lookups, Set masterRecordIds) { // Process lookups, - // Realtime are added to a list for later LRE context creation and processing, + // Realtime & Developer are added to a list for later LRE context creation and processing, // Scheduled result in parent Id's being emitted to scheduled item object for later processing Map gd = Schema.getGlobalDescribe(); - List realtimeLookups = new List(); + List runnowLookups = new List(); List scheduledItems = new List(); for(LookupRollupSummary__c lookup : lookups) { - if(lookup.CalculationMode__c == RollupSummaries.CalculationMode.Realtime.name()) + if(lookup.CalculationMode__c == RollupSummaries.CalculationMode.Realtime.name() || + lookup.CalculationMode__c == RollupSummaries.CalculationMode.Developer.name()) { - // Filter realtime looks in order to generate LRE contexts below - realtimeLookups.add(lookup); + // Filter realtime & Developer lookups in order to generate LRE contexts below + runnowLookups.add(lookup); } else if(lookup.CalculationMode__c == RollupSummaries.CalculationMode.Scheduled.name()) { @@ -600,7 +664,7 @@ global with sharing class RollupService // Process each context (parent child relationship) and its associated rollups Map masterRecords = new Map(); - for(LREngine.Context ctx : createLREngineContexts(realtimeLookups).values()) + for(LREngine.Context ctx : createLREngineContexts(runnowLookups).values()) { // Produce a set of master Id's applicable to this context (parent only) Set ctxMasterIds = new Set(); @@ -634,13 +698,13 @@ global with sharing class RollupService * * @returns List of rollup summary definitions **/ - private static List describeRollups(SObjectType childObjectType) + private static List describeRollups(SObjectType childObjectType, List calculationModes) { // Query applicable lookup definitions Schema.DescribeSObjectResult childRecordDescribe = childObjectType.getDescribe(); List lookups = new RollupSummariesSelector(false).selectActiveByChildObject( - new List { RollupSummaries.CalculationMode.Realtime, RollupSummaries.CalculationMode.Scheduled }, + calculationModes, new Set { childRecordDescribe.getName() }); return lookups; } diff --git a/rolluptool/src/classes/RollupServiceTest3.cls b/rolluptool/src/classes/RollupServiceTest3.cls index 13ee2c42..541f2ff0 100644 --- a/rolluptool/src/classes/RollupServiceTest3.cls +++ b/rolluptool/src/classes/RollupServiceTest3.cls @@ -527,6 +527,539 @@ private with sharing class RollupServiceTest3 System.assertEquals(42, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); } + /** + * Test simulation of isInsert + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumInserted() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, 20); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, 20); + children.add(child2); + SObject child3 = childType.newSObject(); + child3.put(relationshipField, parent.Id); + child3.put(aggregateField, 2); + children.add(child3); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // Call developer 'trigger like' API + RollupService.rollup(null, new Map(children), childType); + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(42, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + + /** + * Test simulation of isUpdate with no field changes + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumUpdatedFieldDoesNotChange() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, 20); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, 20); + children.add(child2); + SObject child3 = childType.newSObject(); + child3.put(relationshipField, parent.Id); + child3.put(aggregateField, 2); + children.add(child3); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // Sample various limits prior to an update + Integer beforeQueries = Limits.getQueries(); + Integer beforeQueryRows = Limits.getQueryRows(); + Integer beforeDMLRows = Limits.getDMLRows(); + + // Call developer 'trigger like' API + // No changes to the field being aggregted, thus no rollup processing should occur + RollupService.rollup(new Map(children), new Map(children), childType); + + // Assert no further limits have been used since the field to aggregate on the detail has not changed + System.assertEquals(beforeQueries + 1, Limits.getQueries()); // Only tolerate a query for the Lookup definition + System.assertEquals(beforeQueryRows + 1, Limits.getQueryRows()); // Only tolerate a row for the Lookup definition + System.assertEquals(beforeDMLRows, Limits.getDMLRows()); // No changes so not record should be operated against + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + + /** + * Test simulation of isUpdate with field changes + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumUpdatedFieldDoesChange() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, 0); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, 0); + children.add(child2); + SObject child3 = childType.newSObject(); + child3.put(relationshipField, parent.Id); + child3.put(aggregateField, 0); + children.add(child3); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // update the children + List modifiedChildren = Database.query(String.format('select id, {0}, {1} from {2}', new List{ aggregateField, relationshipField, childObjectName })); + System.assertEquals(9, modifiedChildren.size()); + for (SObject child :modifiedChildren) + { + child.put(aggregateField, 14); + } + update modifiedChildren; + + // Call developer 'trigger like' API simulating an update + RollupService.rollup(new Map(children), new Map(modifiedChildren), childType); + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(42, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + + /** + * Test simulation of combination of isInsert and isUpdate + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumInsertedWithExistingThatDoNotChange() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, 20); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, 20); + children.add(child2); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // insert a new child + List newChildren = new List(); + for(SObject parent : parents) + { + SObject child3 = childType.newSObject(); + child3.put(relationshipField, parent.Id); + child3.put(aggregateField, 2); + newChildren.add(child3); + } + insert newChildren; + + // Assert nothing has changed on db + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // combine the new and old + List newAndOldChildren = children.clone(); + newAndOldChildren.addAll(newChildren); + + // Call developer 'trigger like' API simulating an update + RollupService.rollup(new Map(children), new Map(newAndOldChildren), childType); + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(42, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(42, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + + /** + * Test simulation of isDelete + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumDeleted() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + Integer child1Amount = 42; + Integer child2Amount = 1; + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, child1Amount); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, child2Amount); + children.add(child2); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // delete existing child that have specified aggregateField value leaving a total to rollup of expected + List deletedChildren = Database.query(String.format('select id, {0}, {1} from {2} WHERE {0} = {3}', new List{ aggregateField, relationshipField, childObjectName, String.valueOf(child2Amount) })); + System.assertEquals(3, deletedChildren.size()); + delete deletedChildren; + + // Assert nothing has changed on db + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // Call developer 'trigger like' API simulating a delete + RollupService.rollup(new Map(deletedChildren), null, childType); + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(child1Amount, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(child1Amount, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(child1Amount, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + + /** + * Test simulation of isUndelete + */ + private testmethod static void testDeveloperTriggerLikeAPI_SingleSumUndeleted() + { + // Test supported? + if(!TestContext.isSupported()) + return; + + Schema.SObjectType parentType = LookupParent__c.sObjectType; + Schema.SObjectType childType = LookupChild__c.sObjectType; + String parentObjectName = parentType.getDescribe().getName(); + String childObjectName = childType.getDescribe().getName(); + String relationshipField = LookupChild__c.LookupParent__c.getDescribe().getName(); + String aggregateField = LookupChild__c.Amount__c.getDescribe().getName(); + String aggregateResultField = LookupParent__c.Total__c.getDescribe().getName(); + Integer child1Amount = 40; + Integer child2Amount = 2; + Integer expectedAmount = 42; + + // Create rollup + LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c(); + rollupSummary.Name = 'Test Rollup'; + rollupSummary.ParentObject__c = parentObjectName; + rollupSummary.ChildObject__c = childObjectName; + rollupSummary.RelationShipField__c = relationshipField; + rollupSummary.FieldToAggregate__c = aggregateField; + rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Sum.name(); + rollupSummary.AggregateResultField__c = aggregateResultField; + rollupSummary.Active__c = true; + rollupSummary.CalculationMode__c = RollupSummaries.CalculationMode.Developer.name(); + insert rollupSummary; + + // Insert parents + SObject parentA = parentType.newSObject(); + parentA.put('Name', 'ParentA'); + SObject parentB = parentType.newSObject(); + parentB.put('Name', 'ParentB'); + SObject parentC = parentType.newSObject(); + parentC.put('Name', 'ParentC'); + List parents = new List { parentA, parentB, parentC }; + insert parents; + + // Insert children + List children = new List(); + for(SObject parent : parents) + { + String name = (String) parent.get('Name'); + SObject child1 = childType.newSObject(); + child1.put(relationshipField, parent.Id); + child1.put(aggregateField, child1Amount); + children.add(child1); + SObject child2 = childType.newSObject(); + child2.put(relationshipField, parent.Id); + child2.put(aggregateField, child2Amount); + children.add(child2); + } + insert children; + + // Assert nothing has changed on db + Map assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // delete existing children that have specified aggregateField value + List childrenToDelete = Database.query(String.format('select id, {0}, {1} from {2} WHERE {0} = {3}', new List{ aggregateField, relationshipField, childObjectName, String.valueOf(child2Amount) })); + System.assertEquals(3, childrenToDelete.size()); + delete childrenToDelete; + + // Assert nothing has changed on db + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // assert remaining children + List remainingChildren = Database.query(String.format('select id, {0}, {1} from {2}', new List{ aggregateField, relationshipField, childObjectName })); + System.assertEquals(3, remainingChildren.size()); + + // retrieve children to undelete + List childrenToUndelete = Database.query(String.format('select id, {0}, {1} from {2} WHERE IsDeleted = true ALL ROWS', new List{ aggregateField, relationshipField, childObjectName })); + System.assertEquals(3, childrenToUndelete.size()); + undelete childrenToUndelete; + + // Assert nothing has changed on db + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(null, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(null, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + + // Call developer 'trigger like' API simulating a undelete + RollupService.rollup(null, new Map(childrenToUndelete), childType); + + // Assert parents are updated + assertParents = new Map(Database.query(String.format('select id, {0} from {1}', new List{ aggregateResultField, parentObjectName }))); + System.assertEquals(3, assertParents.size()); + System.assertEquals(expectedAmount, (Decimal) assertParents.get(parentA.id).get(aggregateResultField)); + System.assertEquals(expectedAmount, (Decimal) assertParents.get(parentB.id).get(aggregateResultField)); + System.assertEquals(expectedAmount, (Decimal) assertParents.get(parentC.id).get(aggregateResultField)); + } + /** * Create test user **/ From c8a1474f9f2ad57e38f95d1de281b697ca8fbad4 Mon Sep 17 00:00:00 2001 From: Jon Davis Date: Wed, 12 Aug 2015 18:20:16 -0700 Subject: [PATCH 2/2] optimize for pure insert/delete/undelete scenarios When there are no oldRecords or there are no newRecords, change detection does not need to occur. Also corrected/updated comments. --- rolluptool/src/classes/RollupService.cls | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/rolluptool/src/classes/RollupService.cls b/rolluptool/src/classes/RollupService.cls index f00af46e..8d037e91 100644 --- a/rolluptool/src/classes/RollupService.cls +++ b/rolluptool/src/classes/RollupService.cls @@ -166,22 +166,23 @@ global with sharing class RollupService * Developer API for the tool, only executes Rollup Summmaries with Calculation Mode set to Developer * * Automatically resolves child records to process via LREngine and lookups described in LookupRollupSummary__c - * also determines if based on the old trigger records if the rollup processing needs to occur + * also determines if based on the old records if the rollup processing needs to occur * - * @param existingRecords Existing records corresponding to the childRecords. - * @param newRecords Child records being modified + * @param existingRecords Deleted or existing version of Updated records + * @param newRecords Inserted/Updated/Undeleted records * @param sObjectType SObjectType of the existing/new records * * @usage rollup(Trigger.oldMap, Trigger.newMap, Account.SObjectType) * * @remark All SObjects (existing and new) must be of the same SObjectType + * @remark Supports mixture of old/new records. For example, you can include a record in existing + * that was deleted and a record in new that was inserted. **/ global static void rollup(Map existingRecords, Map newRecords, Schema.SObjectType sObjectType) { handleRollups(existingRecords, newRecords, sObjectType, new List { RollupSummaries.CalculationMode.Developer }); } - /** * Developer API for the tool, only executes Rollup Summmaries with Calculation Mode set to Developer * @@ -527,8 +528,10 @@ global with sharing class RollupService if(lookups.isEmpty()) return; // Nothing to see here! :) + // if records exist in both maps, then we need to go through change detection. // Has anything changed on the child records in respect to the fields referenced on the lookup definition? - if(existingRecords != null && !existingRecords.isEmpty()) + // Or does a record exist in one map but not the other + if(!existingRecords.isEmpty() && !newRecords.isEmpty()) { // Master records to update Set masterRecordIds = new Set(); @@ -606,9 +609,11 @@ global with sharing class RollupService return; } - // Rollup child records and update master records + // Rollup whichever side has records and update master records + // only one map should have records at this point Set masterRecordIds = new Set(); - for(SObject childRecord : newRecords.values()) + Map recordsToProcess = existingRecords.isEmpty() ? newRecords : existingRecords; + for(SObject childRecord : recordsToProcess.values()) for(LookupRollupSummary__c lookup : lookups) if(childRecord.get(lookup.RelationShipField__c)!=null) masterRecordIds.add((Id)childRecord.get(lookup.RelationShipField__c));