Skip to content

BeforeDelete.Validator ​

Reject a delete before the records are removed, like a validation rule written in Apex.

Signature

apex
public interface Validator extends BeforeDelete.Handler {
    Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record);
    void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record);
}
  • addErrorOnBeforeDeleteWhen(record): called once per record in the chunk. Return true to reject the record.
  • addErrorOnBeforeDelete(record): called right after its predicate returns true; it must attach an error to the record, or the library throws.

Example

apex
public with sharing class ContactValidator implements BeforeDelete.Validator {
    public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) {
        return record.isTrue(Contact.DoNotCall);
    }

    public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) {
        record.addError('A Do Not Call contact cannot be deleted.');
    }
}
apex
public with sharing class AccountDeletionGuardValidator implements BeforeDelete.Validator, BeforeDelete.RelatedQuery {
    public Map<String, BeforeDelete.RecordsProvider> queryRelatedOnBeforeDelete() {
        return new Map<String, BeforeDelete.RecordsProvider>{ 'openOpportunities' => new OpenOpportunitiesProvider() };
    }

    public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) {
        return record.getRelated('openOpportunities').getFirstWhereKeyEquals(record.getId()) != null;
    }

    public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) {
        record.addError('Close or move the open opportunities before you delete this account.');
    }

    private without sharing class OpenOpportunitiesProvider implements BeforeDelete.RecordsProvider {
        public List<SObject> query(TriggerTypes.DeleteRecords records) {
            return [SELECT Id, AccountId FROM Opportunity WHERE AccountId IN :records.getIds() AND IsClosed = FALSE];
        }

        public String keyOf(SObject row) {
            return ((Opportunity) row).AccountId;
        }
    }
}

Rules ​

  • Block with record.addError. addError(message) or addError(field, message) attaches the error to the row being deleted, and its delete fails.
  • Every qualified record must get an error. If addErrorOnBeforeDelete attaches none, the library throws a TriggerTypes.TriggerLibException, even with ContinueOnError.
  • Errors add up. A record with an error still reaches every later handler in the list.
  • Validator wins. A class that also implements BeforeDelete.Writer runs only as a Validator.

WARNING

A guard must see every row. A with sharing provider misses rows the user cannot see, and a missed row lets the delete through. Never add ContinueOnError to a guard: a swallowed exception lets the delete through too.

Test ​

apex
@IsTest
static void addErrorOnBeforeDeleteAttachesError() {
    // Setup
    Account oldAccount = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme');

    // Test
    new AccountDeletionGuardValidator().addErrorOnBeforeDelete(new TriggerTypes.TriggerRecord(null, oldAccount));

    // Verify
    Assert.isTrue(oldAccount.hasErrors(), 'The delete should be blocked.');
}