Skip to content

BeforeUpdate.ParentQuery

Reads fields of the record a lookup points to now, such as a contact's account, without SOQL in your handler. It follows a lookup the user just changed.

Interface

apex
public class BeforeUpdate {
    public interface ParentQuery {
        Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeUpdate();
    }
}
  • queryParentsOnBeforeUpdate(): 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 ContactPopulator implements BeforeUpdate.Populator, BeforeUpdate.ParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Contact.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

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

    public void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record) {
        record.put(Contact.Description, ((Account) record.getNewParent('Account')).Name);
    }
}
cls
public with sharing class OpportunityAccountMoveValidator implements BeforeUpdate.Validator, BeforeUpdate.ParentQuery, BeforeUpdate.PriorParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnBeforeUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Boolean errorShouldBeAttachedOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.equals(Opportunity.StageName, 'Closed Won') && record.isChanged(Opportunity.AccountId);
    }

    public void addErrorOnBeforeUpdate(TriggerHandler.RejectableUpdateRecord record) {
        Account previousAccount = (Account) record.getOldParent('Account');
        Account currentAccount = (Account) record.getNewParent('Account');

        record.addError(
            Opportunity.AccountId,
            'This Opportunity is Closed Won and stays with ' +
                (previousAccount?.Name ?? 'its original Account') +
                '. It cannot be moved to ' +
                (currentAccount?.Name ?? 'another Account') +
                '.'
        );
    }
}
cls
public with sharing class ContactAccountTransferPopulator implements BeforeUpdate.Populator, BeforeUpdate.ParentQuery, BeforeUpdate.PriorParentQuery, BeforeUpdate.RecursionGuard {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Contact.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnBeforeUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Contact.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
    }

    public Integer maxRecursionDepthOnBeforeUpdate() {
        return 1;
    }

    public Boolean populateOnBeforeUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Contact.AccountId) && !record.isChangedFrom(Contact.AccountId, null);
    }

    public void populateOnBeforeUpdate(TriggerHandler.UpdateRecord record) {
        Contact contactRecord = (Contact) record.getNewSObject();
        Account previousAccount = (Account) record.getOldParent('Account');
        Account currentAccount = (Account) record.getNewParent('Account');

        String auditLine = Date.today().format() + ' - transferred from ' + (previousAccount?.Name ?? 'a deleted account') + ' to ' + (currentAccount?.Name ?? 'no account') + '.';

        record.put(Contact.Description, String.isBlank(contactRecord.Description) ? auditLine : contactRecord.Description + '\n' + auditLine);
    }
}

Good to Know

  • Read by relationship name. Use getNewParent('Account') for AccountId and getNewParent('Parent') for ParentId. The name is case-sensitive. The parent is null when the lookup is empty or no record has that Id.
  • Later handlers see the new parent. When a Populator changes the lookup, every handler listed after it gets the new parent.
  • Only declared fields. The parent holds the declared fields and its Id. Reading any other field throws an SObjectException. Add grandparent fields with .with('Owner', User.IsActive).
  • One query per lookup. Each declared lookup costs one SOQL query per chunk, even when no record qualifies. A PriorParentQuery on the same lookup shares that query.
  • No sharing. Parents are read in system mode, so a handler can see records the user cannot.