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')forAccountIdandgetNewParent('Parent')forParentId. 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 anSObjectException. 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.
