Are you an LLM? You can read better optimized documentation at /after-update/add-ons/related-query.md for this page in Markdown format
AfterUpdate.RelatedQuery
Queries children, siblings or any other records once per chunk, and reads them per record by key. No SOQL in the predicate or the action.
Interface
apex
public class AfterUpdate {
public interface RelatedQuery {
Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate();
}
}queryRelatedOnAfterUpdate(): called once per chunk, at this handler’s turn, before its first predicate. Returns provider name →RecordsProvider.
RecordsProvider
apex
public class AfterUpdate {
public interface RecordsProvider {
List<SObject> query(TriggerHandler.UpdateRecords records);
String keyOf(SObject record);
}
}query(records): called once per provider, at the handler’s turn, with every record in the chunk. Returns the rows to index by key;nullcounts as no rows.keyOf(record): called once per row thatqueryreturned. Returns the key thatgetFirstWhereKeyEqualsandgetAllWhereKeyEqualsmatch;nullleaves the row out of the index.
Example
apex
public with sharing class ContactWriter implements AfterUpdate.Writer, AfterUpdate.RelatedQuery {
public Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate() {
return new Map<String, AfterUpdate.RecordsProvider>{ 'accountContacts' => new AccountContactsProvider() };
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChanged(Contact.AccountId);
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Id accountId = ((Contact) record.getNewSObject()).AccountId;
Integer contactCount = record.getRelated('accountContacts').getAllWhereKeyEquals(accountId).size();
unitOfWork.toInsert(new Task(WhatId = accountId, Subject = 'Contacts on the account: ' + contactCount));
}
private with sharing class AccountContactsProvider implements AfterUpdate.RecordsProvider {
public List<SObject> query(TriggerHandler.UpdateRecords records) {
return [SELECT Id, AccountId FROM Contact WHERE AccountId IN :records.getIdsOf(Contact.AccountId)];
}
public String keyOf(SObject record) {
return ((Contact) record).AccountId;
}
}
}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;
}
}
}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 your own providers by name. Call
record.getRelated('contacts').getAllWhereKeyEquals(record.getId()). An unknown name throwsTriggerHandler.TriggerHandlerException, even with ContinueOnError. - Keys match exactly. Keys compare as text, case included. Normalize text keys the same way in
keyOfand in the lookup. - Providers run even when nothing qualifies. They run before the first predicate, with every record in the chunk. Return an empty list from
querywhen no record can qualify. - Both rows.
records.getIdsOf(…)reads the new rows andrecords.getOldIdsOf(…)the old ones. SOQL sees the saved new values. AddId NOT IN :records.getIds()to leave out the trigger records. - Declare sharing on every provider class. A provider's query runs under its own class's sharing keyword. An inner class does not take its outer class's keyword.
