Skip to content

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. Return true to 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

  • put counts as a change. Handlers listed later see the value, and isChanged is 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, isChanged compares with the values the previous update saved. The default limit is 3 passes per record; change it with a RecursionGuard.
  • Never write to getOldSObject(). The FinalException cannot 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.Validator runs 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.');
}