Intermediate
30 min read
Bulkify the Logic
By the end of this lesson, you'll be able to:
- Collect the Account Id and Amount delta for every affected Opportunity
- Query and update the affected Accounts in a single bulk operation
- Apply Module 25's "collect, then act once" pattern to this specific feature
Prerequisites: "Build the Trigger Handler"
Collecting the amount to adjust, per Account
public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) {
Map<Id, Decimal> amountAdjustmentByAccountId = new Map<Id, Decimal>();
for (Opportunity opp : newOpportunities) {
Opportunity oldOpp = oldMap.get(opp.Id);
Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won';
Boolean justUnwon = opp.StageName != 'Closed Won' && oldOpp.StageName == 'Closed Won';
Decimal amount = opp.Amount == null ? 0 : opp.Amount;
if (justWon) {
addAdjustment(amountAdjustmentByAccountId, opp.AccountId, amount);
} else if (justUnwon) {
addAdjustment(amountAdjustmentByAccountId, opp.AccountId, -amount);
}
}
// ... apply the adjustments (next section)
}
private void addAdjustment(Map<Id, Decimal> adjustments, Id accountId, Decimal amount) {
if (! adjustments.containsKey(accountId)) {
adjustments.put(accountId, 0);
}
adjustments.put(accountId, adjustments.get(accountId) + amount);
}
public void afterUpdate(List<Opportunity> newOpportunities, Map<Id, Opportunity> oldMap) {— the method.Map<Id, Decimal> amountAdjustmentByAccountId = new Map<Id, Decimal>();— this is Module 16's "Maps of sObjects" pattern — one entry per affectedAccount.- (blank line)
for (Opportunity opp : newOpportunities) {— loops over the updated records — pure in-memory work so far.Opportunity oldOpp = oldMap.get(opp.Id);— the prior version.Boolean justWon = opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won';— the same transition check as before.Boolean justUnwon = opp.StageName != 'Closed Won' && oldOpp.StageName == 'Closed Won';— the reverse.Decimal amount = opp.Amount == null ? 0 : opp.Amount;— Module 6's ternary operator, defaulting a nullAmountto0.- (blank line)
if (justWon) {— the won case.addAdjustment(amountAdjustmentByAccountId, opp.AccountId, amount);— adds the amount to this Account's running total.} else if (justUnwon) {— the unwon case.addAdjustment(amountAdjustmentByAccountId, opp.AccountId, -amount);— subtracts it back out.}— closes theif/else if.}— closes the loop.// ... apply the adjustments (next section)— the next step, covered separately.}— closes the method.- (blank line)
private void addAdjustment(Map<Id, Decimal> adjustments, Id accountId, Decimal amount) {— a small helper.if (! adjustments.containsKey(accountId)) {— the first time this Account is seen...adjustments.put(accountId, 0);— ...its running total starts at0.}— closes theif.adjustments.put(accountId, adjustments.get(accountId) + amount);— adds this adjustment, so several Opportunities for the same Account in one transaction combine into a single running total.}— closes the helper.
Applying the adjustments in one bulk update
private void applyAdjustments(Map<Id, Decimal> amountAdjustmentByAccountId) {
if (amountAdjustmentByAccountId.isEmpty()) {
return;
}
List<Account> accounts = [
SELECT Id, Total_Won_Revenue__c
FROM Account
WHERE Id IN :amountAdjustmentByAccountId.keySet()
];
for (Account acc : accounts) {
Decimal current = acc.Total_Won_Revenue__c == null ? 0 : acc.Total_Won_Revenue__c;
acc.Total_Won_Revenue__c = current + amountAdjustmentByAccountId.get(acc.Id);
}
update accounts;
}
private void applyAdjustments(Map<Id, Decimal> amountAdjustmentByAccountId) {— the method.if (amountAdjustmentByAccountId.isEmpty()) {— if nothing needs adjusting...return;— ...exits early, avoiding a wasted query and update.}— closes theif.- (blank line)
List<Account> accounts = [— this is Module 25's core pattern applied directly — one query.SELECT Id, Total_Won_Revenue__c— the fields needed.FROM Account— the object.WHERE Id IN :amountAdjustmentByAccountId.keySet()— Module 16'skeySet(), fetching exactly the affected Accounts.];— closes the query.- (blank line)
for (Account acc : accounts) {— loops over the affected accounts, in memory.Decimal current = acc.Total_Won_Revenue__c == null ? 0 : acc.Total_Won_Revenue__c;— defaults a null rollup to0.acc.Total_Won_Revenue__c = current + amountAdjustmentByAccountId.get(acc.Id);— applies this Account's adjustment.}— closes the loop.- (blank line)
update accounts;— oneupdate, regardless of whether 1 or 200 Opportunities changed in this transaction — exactly what makes this feature bulk-safe.}— closes the method.
Exercise
As a comment, explain why amountAdjustmentByAccountId.isEmpty() is checked before running the query and update.
Show hint
Think about a batch of Opportunity updates where none of them actually changed Closed Won status.
APEX
Bulkify the Logic Quiz
My Notes
Log in to keep private notes on this lesson.
Questions about this lesson
No questions yet — be the first to ask.
Log in to ask a question about this lesson.
Summary
This lesson turns the previous lesson's per-record debug statements into a real, bulk-safe rollup — querying and updating Accounts once, regardless of how many Opportunities changed.