AfterUpdate.RecursionGuard
Caps how many times an after update Writer or Dispatcher acts on the same record in one transaction. Without it, the limit is 3. It exists only in the update contexts.
Interface
apex
public class AfterUpdate {
public interface RecursionGuard {
Integer maxRecursionDepthOnAfterUpdate();
}
}maxRecursionDepthOnAfterUpdate(): called once per chunk, when the handler list is built, even for a handler that is then bypassed. Returns how many times one record may qualify for this handler in the transaction.
Example
apex
public with sharing class ContactWriter implements AfterUpdate.Writer, AfterUpdate.RecursionGuard {
public Integer maxRecursionDepthOnAfterUpdate() {
return 1;
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChanged(Contact.AccountId);
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
unitOfWork.toInsert(new Task(WhatId = ((Contact) record.getNewSObject()).AccountId, Subject = 'Review contact'));
}
}cls
public with sharing class OpportunityAccountTypeWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.RecursionGuard {
public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Type) };
}
public Integer maxRecursionDepthOnAfterUpdate() {
return 1;
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
Account parentAccount = (Account) record.getNewParent('Account');
return record.isChangedTo(Opportunity.StageName, 'Closed Won') && parentAccount != null && parentAccount.Type != 'Customer - Direct';
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Opportunity newOpportunity = (Opportunity) record.getNewSObject();
unitOfWork.toUpdate(new Account(Id = newOpportunity.AccountId, Type = 'Customer - Direct'));
}
}cls
public with sharing class AccountAddressCascadeWriter implements AfterUpdate.Writer, AfterUpdate.RelatedQuery, AfterUpdate.ContinueOnError, AfterUpdate.RecursionGuard {
public Integer maxRecursionDepthOnAfterUpdate() {
return 1;
}
public Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate() {
return new Map<String, AfterUpdate.RecordsProvider>{ 'contacts' => new AccountContactsProvider() };
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isAnyChanged(Account.BillingStreet, Account.BillingCity, Account.BillingState, Account.BillingPostalCode, Account.BillingCountry);
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Account newAddress = (Account) record.getNewSObject();
Account previousAddress = (Account) record.getOldSObject();
for (SObject relatedRecord : record.getRelated('contacts').getAllWhereKeyEquals(record.getId())) {
Contact contactRecord = (Contact) relatedRecord;
if (!this.mailingFollowedAccount(contactRecord, previousAddress)) {
continue;
}
unitOfWork.toUpdate(
new Contact(
Id = contactRecord.Id,
MailingStreet = newAddress.BillingStreet,
MailingCity = newAddress.BillingCity,
MailingState = newAddress.BillingState,
MailingPostalCode = newAddress.BillingPostalCode,
MailingCountry = newAddress.BillingCountry
)
);
}
}
private Boolean mailingFollowedAccount(Contact contactRecord, Account previousAddress) {
if (String.isBlank(contactRecord.MailingStreet) && String.isBlank(contactRecord.MailingCity)) {
return true;
}
return contactRecord.MailingStreet == previousAddress.BillingStreet &&
contactRecord.MailingCity == previousAddress.BillingCity &&
contactRecord.MailingPostalCode == previousAddress.BillingPostalCode &&
contactRecord.MailingCountry == previousAddress.BillingCountry;
}
private with sharing class AccountContactsProvider implements AfterUpdate.RecordsProvider {
public List<SObject> query(TriggerHandler.UpdateRecords records) {
return [
SELECT Id, AccountId, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry
FROM Contact
WHERE AccountId IN :records.getIds()
];
}
public String keyOf(SObject record) {
return ((Contact) record).AccountId;
}
}
}Good to Know
- It counts passes, not nesting depth. The count goes up each time the predicate returns true and never resets during the transaction. Separate updates of the same record count too.
- Skipped silently. A record at the limit is skipped before its predicate. Nothing is logged, and the Finalizer does not get it.
- Prefer a change gate.
isChangedorisChangedToin the predicate stops re-entry without counting. - Counted per class name and context. Two instances of one class share one count. BeforeUpdate keeps a separate count.
0or less stops the handler. Every record is skipped.nullmeans no limit.
