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.