BeforeUpdate.Populator
Sets fields on records being updated, usually because another field changed. The values save with the record: no DML and no second save.
Interface
apex
public class BeforeUpdate {
public interface Populator extends Handler {
Boolean populateOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record);
void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record);
}
}populateOnBeforeUpdateWhen(record): called once per record in the chunk; a record that has used up its recursion budget is skipped without a call. Returntrueto populate the record.populateOnBeforeUpdate(record): called right after its predicate returns true, for that record.
Example
apex
public with sharing class ContactPopulator implements BeforeUpdate.Populator {
public Boolean populateOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChanged(Contact.Email);
}
public void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record) {
record.put(Contact.HasOptedOutOfEmail, false);
}
}cls
public with sharing class OpportunityForecastPopulator implements BeforeUpdate.Populator {
public Boolean populateOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
Opportunity newOpportunity = (Opportunity) record.getNewSObject();
return record.isAnyChanged(Opportunity.Amount, Opportunity.StageName) &&
!new Set<String>{ 'Closed Won', 'Closed Lost' }.contains(newOpportunity.StageName) &&
record.lessThan(Opportunity.Amount, 5000);
}
public void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record) {
record.put(Opportunity.ForecastCategoryName, 'Omitted');
}
}cls
public with sharing class AccountShippingSyncPopulator implements BeforeUpdate.Populator {
public Boolean populateOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
Boolean billingMoved = record.isAnyChanged(Account.BillingStreet, Account.BillingCity, Account.BillingState, Account.BillingPostalCode, Account.BillingCountry);
return billingMoved && this.shippingFollowedBilling((Account) record.getOldSObject());
}
public void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record) {
Account accountRecord = (Account) record.getNewSObject();
record.put(Account.ShippingStreet, accountRecord.BillingStreet);
record.put(Account.ShippingCity, accountRecord.BillingCity);
record.put(Account.ShippingState, accountRecord.BillingState);
record.put(Account.ShippingPostalCode, accountRecord.BillingPostalCode);
record.put(Account.ShippingCountry, accountRecord.BillingCountry);
}
private Boolean shippingFollowedBilling(Account oldAccount) {
if (String.isBlank(oldAccount.ShippingStreet) && String.isBlank(oldAccount.ShippingCity)) {
return true;
}
return oldAccount.ShippingStreet == oldAccount.BillingStreet &&
oldAccount.ShippingCity == oldAccount.BillingCity &&
oldAccount.ShippingPostalCode == oldAccount.BillingPostalCode &&
oldAccount.ShippingCountry == oldAccount.BillingCountry;
}
}Good to Know
putcounts as a change. Handlers listed later see the value, andisChangedis true for them. Putting the old value back makes the field unchanged.- Gate on a change. The Populator runs again when the same records are updated again in the transaction. There,
isChangedcompares with the values the previous update saved. The default limit is 3 passes per record; change it with a RecursionGuard. - Never write to
getOldSObject(). TheFinalExceptioncannot be caught and fails the update. - No DML. If the handler runs DML or publishes an event, the library throws. Change other records from an AfterUpdate.Writer.
- Populator wins. A class that also implements
BeforeUpdate.Validatorruns only as a Populator.
Test
apex
@IsTest
static void populateOnBeforeUpdateOmitsForecast() {
// Setup
Opportunity newOpportunity = new Opportunity(Amount = 4000, StageName = 'Prospecting');
// Test
new OpportunityForecastPopulator().populateOnBeforeUpdate(new TriggerHandler.TriggerRecord(newOpportunity, new Opportunity(Amount = 9000)));
// Verify
Assert.areEqual('Omitted', newOpportunity.ForecastCategoryName, 'The forecast category should be Omitted.');
}