Are you an LLM? You can read better optimized documentation at /before-insert/add-ons/related-query.md for this page in Markdown format
BeforeInsert.RelatedQuery
Queries other records once per chunk, such as existing duplicates, contacts on the same account or configuration rows. Read them per record with record.getRelated(name).
Interface
apex
public class BeforeInsert {
public interface RelatedQuery {
Map<String, BeforeInsert.RecordsProvider> queryRelatedOnBeforeInsert();
}
}queryRelatedOnBeforeInsert(): called once per chunk, at this handler’s turn, before its first predicate. Returns provider name →RecordsProvider.
RecordsProvider
apex
public class BeforeInsert {
public interface RecordsProvider {
List<SObject> query(TriggerHandler.InsertRecords 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 ContactPopulator implements BeforeInsert.Populator, BeforeInsert.RelatedQuery {
public Map<String, BeforeInsert.RecordsProvider> queryRelatedOnBeforeInsert() {
return new Map<String, BeforeInsert.RecordsProvider>{ 'accountContacts' => new AccountContactsProvider() };
}
public Boolean populateOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
return record.isNotNull(Contact.AccountId);
}
public void populateOnBeforeInsert(TriggerHandler.InsertRecord record) {
Id accountId = ((Contact) record.getNewSObject()).AccountId;
record.put(Contact.Description, 'Contacts on the account: ' + record.getRelated('accountContacts').getAllWhereKeyEquals(accountId).size());
}
private with sharing class AccountContactsProvider implements BeforeInsert.RecordsProvider {
public List<SObject> query(TriggerHandler.InsertRecords 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 ContactDuplicateEmailValidator implements BeforeInsert.Validator, BeforeInsert.RelatedQuery {
public Map<String, BeforeInsert.RecordsProvider> queryRelatedOnBeforeInsert() {
return new Map<String, BeforeInsert.RecordsProvider>{ 'existingContacts' => new ExistingContactsProvider() };
}
public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
Contact newContact = (Contact) record.getNewSObject();
return record.isNotBlank(Contact.Email) && record.getRelated('existingContacts').getFirstWhereKeyEquals(newContact.Email.toLowerCase()) != null;
}
public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
record.addError(Contact.Email, 'A contact with this email already exists.');
}
private without sharing class ExistingContactsProvider implements BeforeInsert.RecordsProvider {
public List<SObject> query(TriggerHandler.InsertRecords records) {
return [SELECT Id, Email FROM Contact WHERE Email IN :records.getValuesOf(Contact.Email)];
}
public String keyOf(SObject record) {
return ((Contact) record).Email?.toLowerCase();
}
}
}Good to Know
- No Ids yet.
records.getIds()is empty. Filter by field values withrecords.getValuesOf(…)or by lookups withrecords.getIdsOf(…). - SOQL cannot see this save. The new records are not in the database yet. Compare them with each other in a Populator's Finalizer.
- Keys match exactly, case included. Normalize text keys the same way in
keyOfand in the lookup, as[Duplicate email]does withtoLowerCase(). - Providers query even when nothing qualifies. They run before the first predicate. Return an empty list from
querywhen no record can qualify, to skip the SOQL. - Each provider has its own sharing. Declare a keyword on every provider class; an inner class does not inherit it. A
with sharingduplicate check misses records the user cannot see.
