Skip to content

Commit 1dcf37e

Browse files
authored
Merge pull request #5 from jamessimone/v1.0.3-updates
v1.0.3 - last of the initial launch feedback
2 parents c8fbdff + 5298d4b commit 1dcf37e

File tree

4 files changed

+91
-21
lines changed

4 files changed

+91
-21
lines changed

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public static Rollup sumFromTrigger(
203203
SObjectType lookupSobjectType
204204
)
205205

206-
//for using as the "one line of code" and CMDT-driven rollups
206+
// for using as the "one line of code" and CMDT-driven rollups
207207
public static void runFromTrigger()
208208
```
209209

@@ -243,7 +243,37 @@ Rollup.sumFromTrigger(
243243
).runCalc();
244244
```
245245

246-
It's that simple. Note that in order for custom apex solutions that don't use the `batch` static method on `Rollup` to properly start, the `runCalc()` method must also be called. That is, if you only have one rollup operation per object, you'll _always_ need to call `runCalc()` when invoking `Rollup` from a trigger.
246+
It's that simple. Note that in order for custom Apex solutions that don't use the `batch` static method on `Rollup` to properly start, the `runCalc()` method must also be called. That is, if you only have one rollup operation per object, you'll _always_ need to call `runCalc()` when invoking `Rollup` from a trigger.
247+
248+
Another note for when the use of an `Evaluator` class might be necessary -- let's say that you have some slight lookup skew caused by a fallback object in a lookup relationship. This fallback object has thousands of objects tied to it, and updates to it are frequently painful / slow. If you didn't need the rollup for the fallback, you could implement an `Evaluator` to exclude it from being processed:
249+
250+
```java
251+
// again using the example of Opportunities
252+
trigger OpportunityTrigger on Opportunity(before update, after update, before insert, after insert, before delete) {
253+
254+
Rollup.sumFromTrigger(
255+
Opportunity.Amount
256+
Opportunity.AccountId,
257+
Account.Id,
258+
Account.AnnualRevenue,
259+
Account.SObjectType,
260+
new FallbackAccountExcluder()
261+
).runCalc();
262+
263+
public class FallbackAccountExcluder implements Rollup.Evaluator {
264+
public Boolean matches(Object calcItem) {
265+
if((calcItem instanceof Opportunity) == false) {
266+
return false;
267+
}
268+
269+
Opportunity opp = (Opportunity) calcItem;
270+
// there are so many ways you could avoid hard-coding the Id here:
271+
// custom settings, custom metadata, labels, and platform cache, to name a few
272+
return opp.AccountId == 'your fallback Account Id' ? false : true;
273+
}
274+
}
275+
}
276+
```
247277

248278
## Special Considerations
249279

@@ -253,6 +283,16 @@ While pains have been taken to create a solution that's truly one-sized-fits-all
253283

254284
Picklists are a loaded topic in Salesforce. They're not only dropdowns, but the order is supposed to matter! MIN/MAXING on a picklist is supposed to return the deepest possible entry in the picklist (for MAX), or the closest to the top of the picklist (for MIN). If you've studied the aggregate function documentation thoroughly in the Salesforce Developer Docs, this will comes as no surprise - but because the ranking system for picklist differs from the ranking system for other pieces of text, I thought to call it out specifically.
255285

286+
### Recalculations
287+
288+
One of the reasons that `Rollup` can boast of superior performance is that, for many operations, it can perform all of the rolling-up necessary without performing much in the way of queries. There are, as always, exceptions to that rule. "Recalculations" are triggered when certain rollup operations encounter something of interest:
289+
290+
- a MIN operation might find that one of the calculation items supplied to it previously _was_ the minimum value, but is no longer the minimum on an update
291+
- a MAX operation might find that one of the calculation items supplied to it previously _was_ the _maxmimum_ value, but is no longer the max on an update
292+
- ... pretty much any operation involving AVERAGE
293+
294+
In these instances, `Rollup` _does_ requery the calculation object; it also does another loop through the calculation items supplied to it in search of _all_ the values necessary to find the true rollup value. This provides context, more than anything -- the rollup operation should still be lightning fast.
295+
256296
## Commit History & Contributions
257297

258298
This repository comes after the result of [dozens of commits](https://github.com/jamessimone/apex-mocks-stress-test/commits/rollup) on my working repository. You can view the full history of the evolution of `Rollup` there.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apex-rollup",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
55
"repository": {
66
"type": "git",

rollup/main/default/classes/Rollup.cls

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -762,9 +762,6 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
762762
return new RollupAsyncProcessor(RollupInvocationPoint.FROM_TRIGGER);
763763
}
764764

765-
List<SObject> calcItems = getTriggerRecords();
766-
SObjectType sObjectType = calcItems.getSObjectType();
767-
768765
String rollupContext;
769766
Boolean shouldReturn = false;
770767
Map<Id, SObject> oldCalcItems;
@@ -791,9 +788,10 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
791788
rollupInfo.RollupType__c = rollupContext + rollupInfo.RollupType__c;
792789
}
793790

791+
List<SObject> calcItems = getTriggerRecords();
794792
return shouldReturn
795793
? new RollupAsyncProcessor(RollupInvocationPoint.FROM_TRIGGER)
796-
: getRollup(rollupMetadata, sObjectType, calcItems, oldCalcItems, eval, RollupInvocationPoint.FROM_TRIGGER);
794+
: getRollup(rollupMetadata, calcItems.getSObjectType(), calcItems, oldCalcItems, eval, RollupInvocationPoint.FROM_TRIGGER);
797795
}
798796

799797
/** end public-facing section, begin private static helpers */
@@ -831,6 +829,9 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
831829
return oldRecordsMap;
832830
}
833831

832+
// normally, you could use a shortcut to initialize a Set<Id> like this
833+
// by calling new Map<Id, SObject>(currentRecords).keyset() -
834+
// but that code path fails if there are null Ids in the list
834835
Set<Id> currentRecordIds = new Set<Id>();
835836
for (SObject currentRecord : currentRecords) {
836837
if (currentRecord.Id != null) {
@@ -846,13 +847,15 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
846847
SObject existingRecordOrDefault = currentRecord.Id != null && existingOldRecordsMap.containsKey(currentRecord.Id)
847848
? existingOldRecordsMap.get(currentRecord.Id)
848849
: (SObject) Type.forName(sObjectDescribe.getName()).newInstance();
850+
existingRecordOrDefault.Id = currentRecord.Id;
849851
Map<String, SObjectField> fieldTokensForObject = sObjectDescribe.fields.getMap();
850852
SObjectField fieldToken = fieldTokensForObject.get(rollupFieldOnCalcItem);
851853
if (existingRecordOrDefault.get(rollupFieldOnCalcItem) != null || fieldToken.getDescribe().isUpdateable() == false) {
852854
continue;
853855
} else {
854856
existingRecordOrDefault.put(rollupFieldOnCalcItem, FieldInitializer.getDefaultValue(fieldToken));
855857
}
858+
existingOldRecordsMap.put(currentRecord.Id, existingRecordOrDefault);
856859
}
857860
return existingOldRecordsMap;
858861
}
@@ -1440,16 +1443,23 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
14401443
}
14411444

14421445
if (this.shouldShortCircuit && this.isFirstTimeThrough) {
1443-
// if, at any point, short circuiting is enabled, we need to loop back around
1444-
// one last time to ensure no other items in memory will contribute to the final rollup
1446+
/**
1447+
* an example of short circuiting - halfway through the list during a MIN operation,
1448+
* Rollup encounters a calcItem whose previous value equals the current min, but the new value
1449+
* is greater than the min. This triggers a full recalc, since it is at once both possible
1450+
* for items outside the list of calcItems to contain the new min, as WELL as for an item
1451+
* WITHIN the existing calcItems to be the new min. This means we have to go back and re-process
1452+
* the records that were already iterated on to ensure that the current operation successfully accounts
1453+
* for everything in-memory and in the database
1454+
*/
14451455
this.isFirstTimeThrough = false;
1446-
index = 0;
1456+
index = 0; // resets the for-loop
14471457
}
14481458
}
1449-
this.returnVal = this.setReturnValue();
1459+
this.setReturnValue();
14501460
}
14511461

1452-
// all of these are no-ops by default
1462+
// all of these are no-ops by default; child classes opt-in to the rollup types applicable
14531463
public virtual void handleCountDistinct(Op op, SObject calcItem, SObjectField operationField) {
14541464
}
14551465
public virtual void handleUpdateCountDistinct(Op op, SObject calcItem, SObjectField operationField, Map<Id, SObject> oldCalcItems) {
@@ -1474,9 +1484,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
14741484
}
14751485
protected virtual void handleShortCircuit(Op op, SObject calcItem, SObjectField operationField) {
14761486
}
1477-
1478-
protected virtual Object setReturnValue() {
1479-
return this.returnVal;
1487+
protected virtual void setReturnValue() {
14801488
}
14811489

14821490
protected virtual Object calculateNewAggregateValue(Set<Id> excludedItems, Op operation, SObjectField operationField, SObjectType sObjectType) {
@@ -1509,8 +1517,8 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
15091517
}
15101518
}
15111519

1512-
protected override Object setReturnValue() {
1513-
return this.distinctValues.size();
1520+
protected override void setReturnValue() {
1521+
this.returnVal = this.distinctValues.size();
15141522
}
15151523

15161524
protected override void handleShortCircuit(Op op, SObject calcItem, SObjectField operationField) {
@@ -1662,8 +1670,8 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
16621670
}
16631671
}
16641672

1665-
protected override Object setReturnValue() {
1666-
return this.returnDecimal;
1673+
protected override void setReturnValue() {
1674+
this.returnVal = this.returnDecimal;
16671675
}
16681676

16691677
protected virtual override Object calculateNewAggregateValue(Set<Id> excludedItems, Op operation, SObjectField operationField, SObjectType sObjectType) {
@@ -1804,10 +1812,10 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
18041812
this.stringVal = (String) this.returnVal;
18051813
}
18061814

1807-
protected override Object setReturnValue() {
1815+
protected override void setReturnValue() {
18081816
String possibleReturnValue = this.stringVal.normalizeSpace();
18091817
String withoutEndingComma = possibleReturnValue.endsWith(',') ? possibleReturnValue.substring(0, possibleReturnValue.length() - 1) : possibleReturnValue;
1810-
return (withoutEndingComma.startsWith(',') ? withoutEndingComma.substring(1, withoutEndingComma.length()) : withoutEndingComma).trim();
1818+
this.returnVal = (withoutEndingComma.startsWith(',') ? withoutEndingComma.substring(1, withoutEndingComma.length()) : withoutEndingComma).trim();
18111819
}
18121820

18131821
protected override void handleShortCircuit(Op op, SObject calcItem, SObjectField operationField) {

rollup/main/default/classes/RollupTests.cls

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,28 @@ private class RollupTests {
15841584
System.assertEquals(750, updatedAcc.AnnualRevenue, 'SUM AFTER_UPDATE from flow should match diff for Amount');
15851585
}
15861586

1587+
@isTest
1588+
static void shouldCorrectlyInitializeDefaultsWhenOldRecordsDontExistFromFlow() {
1589+
List<Opportunity> opps = new List<Opportunity>{
1590+
new Opportunity(Amount = 1000, Id = generateId(Opportunity.SObjectType)),
1591+
new Opportunity(Amount = 1000, Id = generateId(Opportunity.SObjectType))
1592+
};
1593+
DMLMock mock = getMock(opps);
1594+
opps[1].AccountId = null;
1595+
1596+
Test.startTest();
1597+
List<Rollup.FlowOutput> flowOutputs = Rollup.performRollup(prepareFlowTest(opps, 'UPDATE', 'SUM'));
1598+
Test.stopTest();
1599+
1600+
System.assertEquals(1, flowOutputs.size(), 'Flow ouputs were not provided');
1601+
System.assertEquals('SUCCESS', flowOutputs[0].message);
1602+
System.assertEquals(true, flowOutputs[0].isSuccess);
1603+
1604+
System.assertEquals(1, mock.Records.size(), 'SUM AFTER_UPDATE from flow did not update accounts');
1605+
Account updatedAcc = (Account) mock.Records[0];
1606+
System.assertEquals(1000, updatedAcc.AnnualRevenue, 'SUM AFTER_UPDATE from flow should match diff for Amount');
1607+
}
1608+
15871609
@isTest
15881610
static void shouldBeInvokedSuccessfullyBeforeDeleteFromFlow() {
15891611
List<Opportunity> opps = new List<Opportunity>{ new Opportunity(Amount = 1000) };

0 commit comments

Comments
 (0)