Skip to content

Commit

Permalink
Started wiring up Rollup to RollupRelationshipFieldFinder, added Roll…
Browse files Browse the repository at this point in the history
…up__mdt.GrandparentRelationshipFieldPath__c
  • Loading branch information
jamessimone committed Mar 13, 2021
1 parent ff226d6 commit f60ba1c
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 62 deletions.
1 change: 1 addition & 0 deletions extra-tests/classes/RollupIntegrationTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>{ 'Id', 'Name' },
Account.SObjectType,
new Map<Id, SObject>()
);
Expand Down
26 changes: 21 additions & 5 deletions rollup/main/default/classes/Rollup.cls
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
private Map<SObjectType, Set<String>> lookupObjectToUniqueFieldNames;
private List<SObject> 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;
Expand Down Expand Up @@ -289,6 +290,8 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
}

public virtual Database.QueryLocator start(Database.BatchableContext context) {
// TODO - for batch, RollupRelationshipFieldFinder will end up needing to cache the
// query string to use, stopping before the final query so that the QueryLocator can be built
/**
* for batch, we know 100% for sure there's only 1 SObjectType / Set<String> in the map.
* NB: we have to call "getFieldNamesForRollups" in both the "start" and "execute" methods because
Expand Down Expand Up @@ -370,6 +373,17 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
protected override List<SObject> getExistingLookupItems(Set<String> objIds, Rollup rollup, Set<String> uniqueQueryFieldNames) {
if (objIds.isEmpty()) {
return new List<SObject>();
} 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<String>(uniqueQueryFieldNames), String.valueOf(rollup.lookupFieldOnLookupObject), '=');
Expand Down Expand Up @@ -1505,6 +1519,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
ChangedFieldsOnCalcItem__c,
FullRecalculationDefaultNumberValue__c,
FullRecalculationDefaultStringValue__c,
GrandparentRelationshipFieldPath__c,
IsFullRecordSet__c,
IsRollupStartedFromParent__c,
OrderByFirstLast__c,
Expand All @@ -1521,11 +1536,11 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
meta.LookupFieldOnLookupObject__c = meta.LookupFieldOnLookupObject__r.QualifiedApiName;
meta.RollupFieldOnLookupObject__c = meta.RollupFieldOnLookupObject__r.QualifiedApiName;
}
if (rollupMetadata != null) {
cachedMetadata.addAll(rollupMetadata);
}
}
metadata = cachedMetadata;
if (rollupMetadata != null) {
metadata.addAll(rollupMetadata);
}
} else if (metadataType == RollupControl__mdt.SObjectType) {
metadata = RollupControl__mdt.getAll().deepClone().values();
}
Expand Down Expand Up @@ -1915,9 +1930,8 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
protected void process(List<Rollup> rollups) {
this.getFieldNamesForRollups(rollups);
Map<String, SObject> updatedLookupRecords = new Map<String, SObject>();
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;
Expand Down Expand Up @@ -2026,6 +2040,8 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
lookupFieldToCalcItems.get(key).add(calcItem);
}

// if the lookup key differs from what it was on the old calc item,
// include that value as well so that we can fix reparented records' rollup values
SObject potentialOldCalcItem = rollup.oldCalcItems?.get(calcItem.Id);
if (potentialOldCalcItem != null) {
String oldKey = (String) potentialOldCalcItem.get(rollup.lookupFieldOnCalcItem);
Expand Down
36 changes: 18 additions & 18 deletions rollup/main/default/classes/RollupRelationshipFieldFinder.cls
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ public without sharing class RollupRelationshipFieldFinder {
private final List<String> originalParts;
private final Traversal traversal;
private final SObjectType ultimateParent;
private final String optionalWhereClause;
private final RollupControl__mdt rollupControl;
private final Map<Id, SObject> oldRecords;
private final Set<String> uniqueFinalFieldNames;

private List<String> relationshipParts;
private Boolean isFirstRun = true;

public RollupRelationshipFieldFinder(RollupControl__mdt rollupControl, String relationshipPathName, SObjectType ultimateParent, Map<Id, SObject> oldRecords) {
public RollupRelationshipFieldFinder(RollupControl__mdt rollupControl, String relationshipPathName, Set<String> uniqueFinalFieldNames, SObjectType ultimateParent, Map<Id, SObject> 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];
Expand All @@ -30,17 +31,6 @@ public without sharing class RollupRelationshipFieldFinder {
this.originalParts = new List<String>(this.relationshipParts);
}

public RollupRelationshipFieldFinder(
RollupControl__mdt rollupControl,
String relationshipParts,
SObjectType ultimateParent,
Map<Id, SObject> oldRecords,
String optionalWhereClause
) {
this(rollupControl, relationshipParts, ultimateParent, oldRecords);
this.optionalWhereClause = optionalWhereClause;
}

public class Traversal {
public Boolean isFinished = false;
private final Map<Id, SObject> lookupIdToFinalRecords = new Map<Id, SObject>();
Expand All @@ -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<SObject> getAllParents() {
return this.lookupIdToFinalRecords.values();
}

public Boolean isUltimatelyReparented(SObject record, String relationshipFieldName) {
Expand Down Expand Up @@ -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<String> fieldNames = new List<String>();
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));
Expand Down
73 changes: 37 additions & 36 deletions rollup/main/default/classes/RollupRelationshipFieldFinderTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id, SObject>());
Set<String> uniqueFieldNames = new Set<String>{ 'Name', 'Id' };
RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(
control,
'Account.Name',
uniqueFieldNames,
Account.SObjectType,
new Map<Id, SObject>()
);

RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List<Opportunity>{ opp });

System.assertEquals(parent, traversal.retrieveParent(opp.Id));

finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, new Map<Id, SObject>());
finder = new RollupRelationshipFieldFinder(control, 'Name', uniqueFieldNames, Account.SObjectType, new Map<Id, SObject>());
traversal = finder.getParents(new List<Opportunity>{ opp });

System.assertEquals(parent, traversal.retrieveParent(opp.Id));
Expand All @@ -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<Id, SObject>());
RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(
control,
'Account.Owner.Name',
new Set<String>{ 'Name', 'Id' },
User.SObjectType,
new Map<Id, SObject>()
);
RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List<Opportunity>{ opp });

parent = [SELECT OwnerId FROM Account WHERE Id = :parent.Id];
Expand All @@ -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<Id, SObject>());
RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(
control,
'Account.Owner.Name',
new Set<String>{ 'Name', 'Id' },
User.SObjectType,
new Map<Id, SObject>()
);
RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(new List<Opportunity>{ opp });

System.assertEquals(false, traversal.isFinished, 'Should have bailed early!');
Expand Down Expand Up @@ -78,12 +97,13 @@ private class RollupRelationshipFieldFinderTests {

Map<Id, SObject> oldOpps = new Map<Id, Opportunity>{ opp.Id => new Opportunity(Id = opp.Id, AccountId = intermediateOne.Id) };

RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps);
Set<String> uniqueFieldNames = new Set<String>{ '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');
Expand All @@ -100,7 +120,8 @@ private class RollupRelationshipFieldFinderTests {

Map<Id, SObject> oldOpps = new Map<Id, Opportunity>{ opp.Id => new Opportunity(Id = opp.Id, AccountId = null) };

RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Account.Owner.Name', User.SObjectType, oldOpps);
Set<String> uniqueFieldNames = new Set<String>{ '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');
Expand All @@ -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');
}

Expand All @@ -125,39 +146,13 @@ private class RollupRelationshipFieldFinderTests {
insert opps;

Map<Id, SObject> oldOpps = new Map<Id, SObject>{ 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<String>{ '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<Account>{ 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<Opportunity> opps = new List<Opportunity>{ oppOne, oppTwo };
insert opps;

RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(
control,
'Name',
Account.SObjectType,
new Map<Id, SObject>(),
'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');
Expand All @@ -168,7 +163,13 @@ private class RollupRelationshipFieldFinderTests {
List<Opportunity> opps = new List<Opportunity>{ oppOne, oppTwo };
insert opps;

RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(control, 'Name', Account.SObjectType, new Map<Id, SObject>(), '');
RollupRelationshipFieldFinder finder = new RollupRelationshipFieldFinder(
control,
'Name',
new Set<String>{ 'Name', 'Id' },
Account.SObjectType,
new Map<Id, SObject>()
);
RollupRelationshipFieldFinder.Traversal traversal = finder.getParents(opps);

System.assertEquals(parentOne, traversal.retrieveParent(oppOne.Id), 'First opp parent should not be exluded!');
Expand Down
8 changes: 6 additions & 2 deletions rollup/main/default/classes/RollupTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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!');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
<behavior>Required</behavior>
<field>CalcItem__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>GrandparentRelationshipFieldPath__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
Expand Down Expand Up @@ -155,7 +159,7 @@
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
<summaryLayout>
<masterLabel>00h6g000007qgMp</masterLabel>
<masterLabel>00h54000003XXMf</masterLabel>
<sizeX>4</sizeX>
<sizeY>2</sizeY>
<summaryLayoutItems>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>GrandparentRelationshipFieldPath__c</fullName>
<description>If you are rolling up from grandchildren -&gt; 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 &quot;Opportunity.Account.AnnualRevenue&quot;</description>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<inlineHelpText>If you are rolling up from grandchildren -&gt; 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 &quot;Opportunity.Account.AnnualRevenue&quot;</inlineHelpText>
<label>Grandparent Relationship Field Path</label>
<length>255</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</CustomField>
5 changes: 5 additions & 0 deletions rollup/main/default/profiles/Admin.profile-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
<field>Rollup__mdt.FullRecalculationDefaultStringValue__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Rollup__mdt.GrandparentRelationshipFieldPath__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Rollup__mdt.IsFullRecordSet__c</field>
Expand Down

0 comments on commit f60ba1c

Please sign in to comment.