Skip to content

AfterUpdate.PriorParentQuery

Reads fields of the parent a lookup pointed to before this update, such as the previous owner, without SOQL in your handler.

Interface

apex
public class AfterUpdate {
    public interface PriorParentQuery {
        Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnAfterUpdate();
    }
}
  • queryPriorParentsOnAfterUpdate(): 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.PriorParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnAfterUpdate() {
        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.getOldParent('Account') != null;
    }

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

        unitOfWork.toInsert(new Task(WhatId = previousAccount.Id, Subject = 'Contact left ' + previousAccount.Name));
    }
}
cls
public with sharing class AccountOwnerTransferWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.PriorParentQuery, AfterUpdate.RelatedQuery, AfterUpdate.Bypassable {
    public static Boolean isDisabled = false;

    public Boolean bypassOnAfterUpdateWhen() {
        return isDisabled || System.isBatch();
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.OwnerId => TriggerHandler.ParentFields.with(User.Name) };
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.OwnerId => TriggerHandler.ParentFields.with(User.Name) };
    }

    public Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate() {
        return new Map<String, AfterUpdate.RecordsProvider>{ 'openOpportunities' => new AccountOpenOpportunitiesProvider() };
    }

    public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Account.OwnerId);
    }

    public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Account accountRecord = (Account) record.getNewSObject();
        User previousOwner = (User) record.getOldParent('Owner');
        User newOwner = (User) record.getNewParent('Owner');
        String handoverNote = Date.today().format() + ' - accountRecord handed over from ' + previousOwner?.Name + ' to ' + newOwner?.Name;

        for (SObject relatedRecord : record.getRelated('openOpportunities').getAllWhereKeyEquals(record.getId())) {
            Opportunity opportunityRecord = (Opportunity) relatedRecord;

            unitOfWork.toUpdate(
                new Opportunity(Id = opportunityRecord.Id, OwnerId = accountRecord.OwnerId, Description = this.notedDescription(handoverNote, opportunityRecord.Description))
            );
        }
    }

    private String notedDescription(String handoverNote, String description) {
        return String.isBlank(description) ? handoverNote : handoverNote + '\n' + description;
    }

    private with sharing class AccountOpenOpportunitiesProvider implements AfterUpdate.RecordsProvider {
        public List<SObject> query(TriggerHandler.UpdateRecords records) {
            return [
                SELECT Id, AccountId, Description
                FROM Opportunity
                WHERE AccountId IN :records.getIds() AND IsClosed = FALSE
            ];
        }

        public String keyOf(SObject record) {
            return ((Opportunity) record).AccountId;
        }
    }
}

Good to Know

  • Read with getOldParent. Use getOldParent('Owner') for OwnerId. It is null when the old lookup was empty or the parent no longer exists.
  • Each side needs its own declaration. getOldParent needs PriorParentQuery and getNewParent needs ParentQuery, even when the lookup did not change. Implement both to compare the two parents.
  • Fields merge per lookup. When both declare the same lookup, both parents carry every declared field. An unchanged lookup returns the same record on both sides.
  • Current values. The previous parent is queried now, so its fields show today's values.
  • One query per lookup. Previous parents that are not loaded yet cost one query per lookup per chunk, even when no record qualifies. They are read in system mode without sharing.