From f60ba1cd77db40abb6d11e7e94fb62ed7bf618ca Mon Sep 17 00:00:00 2001 From: james simone Date: Sat, 13 Mar 2021 08:35:12 -0700 Subject: [PATCH] Started wiring up Rollup to RollupRelationshipFieldFinder, added Rollup__mdt.GrandparentRelationshipFieldPath__c --- .../classes/RollupIntegrationTests.cls | 1 + rollup/main/default/classes/Rollup.cls | 26 +++++-- .../classes/RollupRelationshipFieldFinder.cls | 36 ++++----- .../RollupRelationshipFieldFinderTests.cls | 73 ++++++++++--------- rollup/main/default/classes/RollupTests.cls | 8 +- .../Rollup__mdt-Rollup Layout.layout-meta.xml | 6 +- ...entRelationshipFieldPath__c.field-meta.xml | 13 ++++ .../default/profiles/Admin.profile-meta.xml | 5 ++ 8 files changed, 106 insertions(+), 62 deletions(-) create mode 100644 rollup/main/default/objects/Rollup__mdt/fields/GrandparentRelationshipFieldPath__c.field-meta.xml diff --git a/extra-tests/classes/RollupIntegrationTests.cls b/extra-tests/classes/RollupIntegrationTests.cls index abd9caa9..8c667cf6 100644 --- a/extra-tests/classes/RollupIntegrationTests.cls +++ b/extra-tests/classes/RollupIntegrationTests.cls @@ -202,6 +202,7 @@ private class RollupIntegrationTests { RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( new RollupControl__mdt(MaxQueryRows__c = 1000), 'Application__r.ParentApplication__r.Account__r.Name', + new Set{ 'Id', 'Name' }, Account.SObjectType, new Map() ); diff --git a/rollup/main/default/classes/Rollup.cls b/rollup/main/default/classes/Rollup.cls index f3446fb9..f82c3bab 100644 --- a/rollup/main/default/classes/Rollup.cls +++ b/rollup/main/default/classes/Rollup.cls @@ -53,6 +53,7 @@ global without sharing virtual class Rollup implements Database.Batchable> lookupObjectToUniqueFieldNames; private List lookupItems; private RollupControl__mdt rollupControl; + private RollupRelationshipFieldFinder.Traversal traversal; /** * receiving an interface/subclass from a property get/set (from the book "The Art Of Unit Testing") is an old technique; @@ -289,6 +290,8 @@ global without sharing virtual class Rollup implements Database.Batchable in the map. * NB: we have to call "getFieldNamesForRollups" in both the "start" and "execute" methods because @@ -370,6 +373,17 @@ global without sharing virtual class Rollup implements Database.Batchable getExistingLookupItems(Set objIds, Rollup rollup, Set uniqueQueryFieldNames) { if (objIds.isEmpty()) { return new List(); + } else if (String.isNotBlank(rollup.metadata.GrandparentRelationshipFieldPath__c)) { + // TODO handle if traversal doesn't finish + rollup.traversal = new RollupRelationshipFieldFinder( + rollup.rollupControl, + rollup.metadata.GrandparentRelationshipFieldPath__c, + uniqueQueryFieldNames, + rollup.lookupObj, + rollup.oldCalcItems + ) + .getParents(rollup.calcItems); + return rollup.traversal.getAllParents(); } // non-obvious coupling between "objIds" and the computed "queryString", which uses dynamic variable binding String queryString = getQueryString(rollup.lookupObj, new List(uniqueQueryFieldNames), String.valueOf(rollup.lookupFieldOnLookupObject), '='); @@ -1505,6 +1519,7 @@ global without sharing virtual class Rollup implements Database.Batchable rollups) { this.getFieldNamesForRollups(rollups); Map updatedLookupRecords = new Map(); - for (Integer index = 0; index < rollups.size(); index++) { + for (Rollup rollup : rollups) { // for each iteration, ensure we're not operating beyond the bounds of our query limits - Rollup rollup = rollups[index]; if (this.hasExceededCurrentRollupQueryLimit(rollup.rollupControl)) { this.deferredRollups.add(rollup); continue; @@ -2026,6 +2040,8 @@ global without sharing virtual class Rollup implements Database.Batchable originalParts; private final Traversal traversal; private final SObjectType ultimateParent; - private final String optionalWhereClause; private final RollupControl__mdt rollupControl; private final Map oldRecords; + private final Set uniqueFinalFieldNames; private List relationshipParts; private Boolean isFirstRun = true; - public RollupRelationshipFieldFinder(RollupControl__mdt rollupControl, String relationshipPathName, SObjectType ultimateParent, Map oldRecords) { + public RollupRelationshipFieldFinder(RollupControl__mdt rollupControl, String relationshipPathName, Set uniqueFinalFieldNames, SObjectType ultimateParent, Map oldRecords) { this.traversal = new Traversal(this); this.relationshipParts = relationshipPathName.split('\\.'); this.rollupControl = rollupControl; this.ultimateParent = ultimateParent; this.oldRecords = oldRecords; + this.uniqueFinalFieldNames = uniqueFinalFieldNames; if (this.relationshipParts.size() == 1) { String fieldName = this.relationshipParts[0]; @@ -30,17 +31,6 @@ public without sharing class RollupRelationshipFieldFinder { this.originalParts = new List(this.relationshipParts); } - public RollupRelationshipFieldFinder( - RollupControl__mdt rollupControl, - String relationshipParts, - SObjectType ultimateParent, - Map oldRecords, - String optionalWhereClause - ) { - this(rollupControl, relationshipParts, ultimateParent, oldRecords); - this.optionalWhereClause = optionalWhereClause; - } - public class Traversal { public Boolean isFinished = false; private final Map lookupIdToFinalRecords = new Map(); @@ -53,7 +43,11 @@ public without sharing class RollupRelationshipFieldFinder { } public SObject retrieveParent(Id descendantId) { - return lookupIdToFinalRecords.get(descendantId); + return this.lookupIdToFinalRecords.get(descendantId); + } + + public List getAllParents() { + return this.lookupIdToFinalRecords.values(); } public Boolean isUltimatelyReparented(SObject record, String relationshipFieldName) { @@ -190,11 +184,17 @@ public without sharing class RollupRelationshipFieldFinder { String nextFieldToLookup = this.relationshipParts[0].replace('__r', '__c'); SObjectType nextSObjectType = firstId.getSObjectType(); SObjectField nextFieldToken = this.getField(nextSObjectType.getDescribe().fields.getMap(), nextFieldToLookup); - // NB - we only support one route through polymorphic fields such as Task.WhoId and Task.WhatId for this sort of thing - String query = 'SELECT Id, ' + nextFieldToken.getDescribe().getName() + ' FROM ' + nextSObjectType.getDescribe().getName() + ' WHERE Id = :lookupIds'; - if (nextSObjectType == this.ultimateParent && String.isNotBlank(this.optionalWhereClause)) { - query += '\nAND ' + this.optionalWhereClause; + List fieldNames = new List(); + if(nextSObjectType == this.ultimateParent) { + fieldNames.addAll(this.uniqueFinalFieldNames); + } else { + fieldNames.add(nextFieldToken.getDescribe().getName()); } + if(fieldNames.contains('Id') == false) { + fieldNames.add('Id'); + } + // NB - we only support one route through polymorphic fields such as Task.WhoId and Task.WhatId for this sort of thing + String query = 'SELECT ' + String.join(fieldNames, ',') + ' FROM ' + nextSObjectType.getDescribe().getName() + ' WHERE Id = :lookupIds'; // recurse through till we get to the top/bottom of the chain this.isFirstRun = false; return this.getParents(Database.query(query)); diff --git a/rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls b/rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls index 5ed86976..555adde9 100644 --- a/rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls +++ b/rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls @@ -10,13 +10,20 @@ private class RollupRelationshipFieldFinderTests { Opportunity opp = new Opportunity(AccountId = parent.Id, Name = 'Child opp', StageName = 'Prospecting', CloseDate = System.today()); insert opp; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Name', Account.SObjectType, new Map()); + Set uniqueFieldNames = new Set{ 'Name', 'Id' }; + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( + control, + 'Account.Name', + uniqueFieldNames, + Account.SObjectType, + new Map() + ); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List{ opp }); System.assertEquals(parent, traversal.retrieveParent(opp.Id)); - finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, new Map()); + finder = new RollupRelationshipFieldFinder(control, 'Name', uniqueFieldNames, Account.SObjectType, new Map()); traversal = finder.getParents(new List{ opp }); System.assertEquals(parent, traversal.retrieveParent(opp.Id)); @@ -30,7 +37,13 @@ private class RollupRelationshipFieldFinderTests { Opportunity opp = new Opportunity(AccountId = parent.Id, Name = 'Child opp looking up to account', StageName = 'Prospecting', CloseDate = System.today()); insert opp; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, new Map()); + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( + control, + 'Account.Owner.Name', + new Set{ 'Name', 'Id' }, + User.SObjectType, + new Map() + ); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List{ opp }); parent = [SELECT OwnerId FROM Account WHERE Id = :parent.Id]; @@ -49,7 +62,13 @@ private class RollupRelationshipFieldFinderTests { Opportunity opp = new Opportunity(AccountId = acc.Id, Name = 'Child opp'); control.MaxQueryRows__c = 1; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, new Map()); + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( + control, + 'Account.Owner.Name', + new Set{ 'Name', 'Id' }, + User.SObjectType, + new Map() + ); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List{ opp }); System.assertEquals(false, traversal.isFinished, 'Should have bailed early!'); @@ -78,12 +97,13 @@ private class RollupRelationshipFieldFinderTests { Map oldOpps = new Map{ opp.Id => new Opportunity(Id = opp.Id, AccountId = intermediateOne.Id) }; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps); + Set uniqueFieldNames = new Set{ 'Name', 'Id' }; + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', uniqueFieldNames, User.SObjectType, oldOpps); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps); System.assertEquals(false, traversal.isUltimatelyReparented(opp, 'AccountId'), 'Should not report false positive!'); - finder = new RollupRelationshipFieldFinder(control, 'Account.Name', Account.SObjectType, oldOpps); + finder = new RollupRelationshipFieldFinder(control, 'Account.Name', uniqueFieldNames, Account.SObjectType, oldOpps); traversal = finder.getParents(opps); System.assertEquals(true, traversal.isUltimatelyReparented(opp, 'AccountId'), 'Should correctly report reparenting if ultimate lookup is different'); @@ -100,7 +120,8 @@ private class RollupRelationshipFieldFinderTests { Map oldOpps = new Map{ opp.Id => new Opportunity(Id = opp.Id, AccountId = null) }; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps); + Set uniqueFieldNames = new Set{ 'Id', 'Name' }; + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', uniqueFieldNames, User.SObjectType, oldOpps); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps); System.assertEquals(true, traversal.isUltimatelyReparented(opp, 'AccountId'), 'Should correctly report reparenting if old lookup null'); @@ -109,7 +130,7 @@ private class RollupRelationshipFieldFinderTests { opp.AccountId = null; update opp; - finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps); + finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', uniqueFieldNames, User.SObjectType, oldOpps); System.assertEquals(true, traversal.isUltimatelyReparented(opp, 'AccountId'), 'Should correctly report reparenting if new lookup is null'); } @@ -125,39 +146,13 @@ private class RollupRelationshipFieldFinderTests { insert opps; Map oldOpps = new Map{ oppOne.Id => oppOne, oppTwo.Id => new Opportunity(AccountId = parentTwo.Id) }; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, oldOpps); + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Name', new Set{ 'Name', 'Id' }, Account.SObjectType, oldOpps); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps); System.assertEquals(true, traversal.isUltimatelyReparented(oppTwo, 'AccountId')); System.assertEquals(false, traversal.isUltimatelyReparented(oppOne, 'AccountId')); } - @isTest - static void shouldOnlyIncludeWhereClauseForLastObjectInChain() { - Account parentOne = new Account(Name = 'ParentToInclude'); - Account parentTwo = new Account(Name = 'ParentToExclude'); - insert new List{ parentOne, parentTwo }; - - Opportunity oppOne = new Opportunity(AccountId = parentOne.Id, Name = 'ChildWithIncludedParent', StageName = 'Prospecting', CloseDate = System.today()); - Opportunity oppTwo = new Opportunity(AccountId = parentTwo.Id, Name = 'ChildWithExcludedParent', StageName = 'Prospecting', CloseDate = System.today()); - List opps = new List{ oppOne, oppTwo }; - insert opps; - - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( - control, - 'Name', - Account.SObjectType, - new Map(), - 'Name != \'' + - parentTwo.Name + - '\'' - ); - RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps); - - System.assertEquals(parentOne, traversal.retrieveParent(oppOne.Id), 'First opp parent should not be exluded!'); - System.assertEquals(null, traversal.retrieveParent(oppTwo.Id), 'Second opp should have been excluded!'); - } - @isTest static void shouldTrackMultipleParents() { Account parentOne = new Account(Name = 'SoloParent'); @@ -168,7 +163,13 @@ private class RollupRelationshipFieldFinderTests { List opps = new List{ oppOne, oppTwo }; insert opps; - RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, new Map(), ''); + RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder( + control, + 'Name', + new Set{ 'Name', 'Id' }, + Account.SObjectType, + new Map() + ); RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps); System.assertEquals(parentOne, traversal.retrieveParent(oppOne.Id), 'First opp parent should not be exluded!'); diff --git a/rollup/main/default/classes/RollupTests.cls b/rollup/main/default/classes/RollupTests.cls index 17f65ac2..51b3afa3 100644 --- a/rollup/main/default/classes/RollupTests.cls +++ b/rollup/main/default/classes/RollupTests.cls @@ -2930,12 +2930,16 @@ private class RollupTests { LookupObject__c = 'User', LookupFieldOnLookupObject__c = 'Id', RollupFieldOnLookupObject__c = 'AboutMe', - RollupOperation__c = 'CONCAT' - // TODO - time to add in new field for relationship name path + RollupOperation__c = 'CONCAT', + GrandparentRelationshipFieldPath__c = 'Account.Owner.AboutMe' ) }; Rollup.apexContext = TriggerOperation.AFTER_INSERT; + Test.startTest(); + Rollup.runFromTrigger(); + Test.stopTest(); + System.assertEquals(1, mock.Records.size(), 'Grandparent record should have been found!'); User updatedUser = (User) mock.Records[0]; System.assertEquals(opps[0].Name + ', ' + opps[1].Name, updatedUser.AboutMe, 'Grandparent rollup should have worked!'); diff --git a/rollup/main/default/layouts/Rollup__mdt-Rollup Layout.layout-meta.xml b/rollup/main/default/layouts/Rollup__mdt-Rollup Layout.layout-meta.xml index e1e03c2b..00da3afc 100644 --- a/rollup/main/default/layouts/Rollup__mdt-Rollup Layout.layout-meta.xml +++ b/rollup/main/default/layouts/Rollup__mdt-Rollup Layout.layout-meta.xml @@ -56,6 +56,10 @@ Required CalcItem__c + + Edit + GrandparentRelationshipFieldPath__c + @@ -155,7 +159,7 @@ false false - 00h6g000007qgMp + 00h54000003XXMf 4 2 diff --git a/rollup/main/default/objects/Rollup__mdt/fields/GrandparentRelationshipFieldPath__c.field-meta.xml b/rollup/main/default/objects/Rollup__mdt/fields/GrandparentRelationshipFieldPath__c.field-meta.xml new file mode 100644 index 00000000..ec2369f6 --- /dev/null +++ b/rollup/main/default/objects/Rollup__mdt/fields/GrandparentRelationshipFieldPath__c.field-meta.xml @@ -0,0 +1,13 @@ + + + GrandparentRelationshipFieldPath__c + If you are rolling up from grandchildren -> grandparent (or greater), supply the field path to the ultimate parent field. As an example, if you were rolling up from Opp Line Items to Account AnnualRevenue, you would do "Opportunity.Account.AnnualRevenue" + false + DeveloperControlled + If you are rolling up from grandchildren -> grandparent (or greater), supply the field path to the ultimate parent field. As an example, if you were rolling up from Opp Line Items to Account AnnualRevenue, you would do "Opportunity.Account.AnnualRevenue" + + 255 + false + Text + false + diff --git a/rollup/main/default/profiles/Admin.profile-meta.xml b/rollup/main/default/profiles/Admin.profile-meta.xml index 1513e598..f8c1245f 100644 --- a/rollup/main/default/profiles/Admin.profile-meta.xml +++ b/rollup/main/default/profiles/Admin.profile-meta.xml @@ -97,6 +97,11 @@ Rollup__mdt.FullRecalculationDefaultStringValue__c true + + true + Rollup__mdt.GrandparentRelationshipFieldPath__c + true + true Rollup__mdt.IsFullRecordSet__c