Are you an LLM? You can read better optimized documentation at /before-delete/add-ons/related-query.md for this page in Markdown format
BeforeDelete.RelatedQuery
Loads children, siblings or other records with one query per provider, instead of SOQL in your handler. The rows being deleted are still linked, so their children can be found.
Interface
apex
public class BeforeDelete {
public interface RelatedQuery {
Map<String, BeforeDelete.RecordsProvider> queryRelatedOnBeforeDelete();
}
}queryRelatedOnBeforeDelete(): called once per chunk, at this handler’s turn, before its first predicate. Returns provider name →RecordsProvider.
RecordsProvider
apex
public class BeforeDelete {
public interface RecordsProvider {
List<SObject> query(TriggerHandler.DeleteRecords 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 ContactHandler implements BeforeDelete.Handler, BeforeDelete.RelatedQuery {
public Map<String, BeforeDelete.RecordsProvider> queryRelatedOnBeforeDelete() {
return new Map<String, BeforeDelete.RecordsProvider>{ 'accountContacts' => new AccountContactsProvider() };
}
public Boolean qualifiesForBeforeDeleteWhen(TriggerHandler.DeleteRecord record) {
return record.isNotNull(Contact.AccountId);
}
public void onBeforeDelete(TriggerHandler.DeleteRecord record) {
Id accountId = ((Contact) record.getOldSObject()).AccountId;
if (record.getRelated('accountContacts').getAllWhereKeyEquals(accountId).size() == 1) {
record.getOldSObject().addError('The last contact of an account cannot be deleted.');
}
}
private with sharing class AccountContactsProvider implements BeforeDelete.RecordsProvider {
public List<SObject> query(TriggerHandler.DeleteRecords records) {
return [SELECT Id, AccountId FROM Contact WHERE AccountId IN :records.getIdsOf(Contact.AccountId)];
}
public String keyOf(SObject record) {
return ((Contact) record).AccountId;
}
}
}apex
public with sharing class AccountDeletionGuardHandler implements BeforeDelete.Handler, BeforeDelete.RelatedQuery {
public Map<String, BeforeDelete.RecordsProvider> queryRelatedOnBeforeDelete() {
return new Map<String, BeforeDelete.RecordsProvider>{ 'openOpportunities' => new OpenOpportunitiesProvider() };
}
public Boolean qualifiesForBeforeDeleteWhen(TriggerHandler.DeleteRecord record) {
return record.getRelated('openOpportunities').getFirstWhereKeyEquals(record.getId()) != null;
}
public void onBeforeDelete(TriggerHandler.DeleteRecord record) {
record.getOldSObject().addError('Close or move the open opportunities before you delete this account.');
}
private without sharing class OpenOpportunitiesProvider implements BeforeDelete.RecordsProvider {
public List<SObject> query(TriggerHandler.DeleteRecords records) {
return [SELECT Id, AccountId FROM Opportunity WHERE AccountId IN :records.getIds() AND IsClosed = FALSE];
}
public String keyOf(SObject record) {
return ((Opportunity) record).AccountId;
}
}
}Good to Know
- Read by provider name. Call
record.getRelated('<name>'). An unknown name throwsTriggerHandler.TriggerHandlerException. - Counts include the records being deleted. A query here still returns them. To count what will remain, add
Id NOT IN :records.getIds(). - Last chance for cascade children. Children removed by a cascade delete run no delete triggers of their own. Read them here.
- Sharing can hide rows. A provider uses its own class's sharing keyword. A
with sharingguard misses rows the user cannot see, so those deletes go through. Usewithout sharingwhen it must see every row. - Runs even when nothing qualifies. Providers run before the first predicate, so they cost their SOQL on every chunk. Return an empty list from
querywhen no record can qualify.
