Skip to content

BeforeInsert.ParentQuery

Reads fields of the record a lookup points to, such as a contact's account, without SOQL in your handler. The row being inserted holds only the lookup Id.

Interface

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

    public Boolean populateOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
        return record.getNewParent('Account') != null;
    }

    public void populateOnBeforeInsert(TriggerHandler.InsertRecord record) {
        record.put(Contact.Description, ((Account) record.getNewParent('Account')).Name);
    }
}
cls
public with sharing class AccountParentDefaultsPopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeInsert() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.ParentId => TriggerHandler.ParentFields.with(Account.Industry, Account.AccountSource) };
    }

    public Boolean populateOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
        return record.isNotNull(Account.ParentId) && (record.isBlank(Account.Industry) || record.isBlank(Account.AccountSource));
    }

    public void populateOnBeforeInsert(TriggerHandler.InsertRecord record) {
        Account parent = (Account) record.getNewParent('Parent');

        if (parent == null) {
            return;
        }

        if (record.isBlank(Account.Industry)) {
            record.put(Account.Industry, parent.Industry);
        }

        if (record.isBlank(Account.AccountSource)) {
            record.put(Account.AccountSource, parent.AccountSource);
        }
    }
}
cls
public with sharing class AccountParentTypeValidator implements BeforeInsert.Validator, BeforeInsert.ParentQuery {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnBeforeInsert() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.ParentId => TriggerHandler.ParentFields.with(Account.Name, Account.Type) };
    }

    public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
        Account parent = (Account) record.getNewParent('Parent');

        return record.isNotNull(Account.ParentId) && parent?.Type == 'Prospect';
    }

    public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
        Account parent = (Account) record.getNewParent('Parent');

        record.addError(Account.ParentId, 'Parent account "' + parent.Name + '" is still a Prospect and cannot head a hierarchy. Qualify it first.');
    }
}

Good to Know

  • Read by relationship name. Use getNewParent('Account') for AccountId and getNewParent('Parent') for ParentId. The name is case-sensitive.
  • Check for null. The parent is null when the lookup is empty or no record has that Id.
  • 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. When a Populator re-points a lookup to a parent not loaded yet, one more query loads it.
  • No sharing. Parents are read in system mode, so a handler can see records the user cannot.