Skip to content

AfterUpdate.ParentQuery

Reads fields of the record a lookup points to now, such as an opportunity's account, without SOQL in your handler. The saved row holds only the lookup Id.

Interface

apex
public class AfterUpdate {
    public interface ParentQuery {
        Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate();
    }
}
  • queryParentsOnAfterUpdate(): called once per chunk, before the first handler runs; not called for a bypassed handler. Returns lookup field → the parent fields to load.

Example

apex
public with sharing class ContactWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Contact.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Contact.AccountId) && record.getNewParent('Account') != null;
    }

    public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Account accountRecord = (Account) record.getNewParent('Account');

        unitOfWork.toInsert(new Task(WhatId = accountRecord.Id, Subject = 'Review contact of ' + accountRecord.Name));
    }
}
cls
public with sharing class OpportunityWinTaskWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.ContinueOnError {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChangedTo(Opportunity.StageName, 'Closed Won');
    }

    public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Opportunity newOpportunity = (Opportunity) record.getNewSObject();
        Account parentAccount = (Account) record.getNewParent('Account');

        unitOfWork.toInsert(
            new Task(
                WhatId = record.getId(),
                OwnerId = newOpportunity.OwnerId,
                Subject = 'Onboarding kick-off: ' + (parentAccount?.Name ?? newOpportunity.Name),
                Description = 'Won on ' + Date.today().format() + '. Agree the implementation plan with the customer.',
                ActivityDate = Date.today().addDays(7),
                Priority = 'High',
                Status = 'Not Started'
            )
        );
    }
}
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'));
    }
}

Good to Know

  • Read by relationship name. Use getNewParent('Account') for AccountId and getNewParent('Owner') for OwnerId. The name is case-sensitive.
  • Check for null. The parent is null when the lookup is empty or no record has that Id.
  • Declare every field you read. The parent holds only the fields the active handlers declared. Reading any other field throws an SObjectException. Add grandparent fields with .with('Owner', User.IsActive).
  • One query per chunk. One query on the trigger records loads every declared parent, even when no record qualifies. A parent it does not return costs one more query for that lookup.
  • No sharing. Parents are read in system mode, so a handler can see records the user cannot. For the parent before the update, use PriorParentQuery.