5

This is a canonical question and answer developed by the community to help address common questions. If you've been directed here, or your question has been closed as a duplicate, please look through the resources here and use them to shape more specific questions. To browse all canonical questions and answers, navigate to the canonical-qa tag.

This question and answer is owned by the community! If you can improve it and make it more useful, you're welcome to edit it.

I am building a trigger, a unit test, or other Apex code. I'm working with sObject records, and trying to access related record information. For example, I might have a structure like this:

Account a = new Account(Name = 'Test');
insert a;

Opportunity o = new Opportunity(Name = 'Donation', AccountId = a.Id);
insert o;

System.assertEquals(o.Account.Name, 'Test', 'Correct Account associated'); // Assertion fails!

Or, in a trigger, I might be aiming for something like this:

trigger ContactTrigger on Contact (after insert) {
    for (Contact c : Trigger.new) {
        if (c.Account.Name == 'Test') {
            // Take some action here
        }
     }
}

Why doesn't this work? Why am I getting null values, or NullPointerExceptions?

1 Answer 1

8

Everywhere in Apex, it is a fact that you must query relationship data you wish to reference.

  • When you insert a record, the only field in your object that is modified by the Apex runtime is the Id: no relationship data is populated, not even to metadata objects like RecordType, and not even to other sObjects you're working with in your local code unit. You must re-query to obtain related record information.
  • When you receive an object from a trigger context variable, you are not provided with any related-record data. You must perform a query to obtain that data. Note that cross-object formula fields in your trigger context variable will be populated.

If you fail to do so and traverse a relationship, you will get back a null value. If you write code that attempts to act on the returned value, you risk a NullPointerException, for example,

if (o.Account.Name.startsWith('HIGH')) {

Examples

General Apex

Account a = new Account(Name = 'Test');
insert a;

Opportunity o = new Opportunity(Name = 'Donation', AccountId = a.Id);
insert o;

o = [SELECT Id, Account.Name FROM Opportunity WHERE Id = :o.Id];

System.assertEquals(o.Account.Name, 'Test', 'Correct Account associated'); // Assertion passes!

We re-query the inserted object to acquire the desired relationship data, and here, the assertion passed.

Apex Triggers

There are slightly different patterns that may be applied in before and after triggers. Starting with an after trigger, as above, we'd rebuild the trigger something like this:

trigger ContactTrigger on Contact (after insert) {
    for (Contact c : [SELECT Id, Account.Name FROM Contact WHERE Id IN :Trigger.new]) {
        if (c.Account.Name == 'Test') {
            // Take some action here
        }
     }
}

We simply replace iteration over Trigger.new (and similar structures) with iteration over a query, which sources relationship data as well as other required fields and which is limited to the trigger context set.

In a before insert context, we can't do this, because our trigger context variables are not yet committed to the database and hence cannot be queried. In this context, we must query the related records directly, using collections like Set<Id> and Map<Id, sObject> to bulkify the code and connect our trigger context variables with queried related records:

trigger ContactTrigger on Contact (before insert) {
    Set<Id> accountIds = new Set<Id>();

    for (Contact c : Trigger.new) {
        if (c.AccountId != null) {
            accountIds.add(a.AccountId);
        }
    }
    Map<Id, Account> accountMap = new Map<Id, Account>(
        [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
    );
    for (Contact c : Trigger.new) {
        if (c.AccountId != null && accountMap.get(c.AccountId).Name == 'Test') {
            // Take some action here
        }
     }
}

Note that in this case we must do additional work to guard against null relationships.

1
  • It appears that Salesforce has some sort of built-in protection mechanism when accessing related records, as if the "Safe Navigation" operator is being used implicitly. For example, while o.Account.Name.startsWith('HIGH') might throw an exception, it appears that o.Account.Name == 'HIGH' will not. Salesforce seems to just report both o.Account and o.Account.Name as null. I can't actually find documentation of this behavior anywhere, and would appreciate if anyone else in the community has more insight.
    – Jax
    Commented Jun 24 at 21:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .