The only way to get data is to either query for it, or create it. That's just the way it is.
The other answers (so far) have gone into how you can write a query to get at the data you're seeking, but I'd like to dive deeper into answering your question about why your experiment returned null values.
Things you need to know to understand why your experiment resulted in nulls
In Salesforce, there are two main domains that data can reside in. The first is 'in-storage', which for our purposes is just another way of saying "it's stored in a database". The second is 'in-memory', which is everything that you have in an Apex variable at a given point in your code (I won't go into the concept of variable 'scope' here, as that's getting overbroad).
To use data in Apex, the data needs to be in-memory. Data is taken from in-storage and put into memory when we query for it. Data is put in-storage from in-memory when we execute DML.
The other thing that you'll need to know to understand why your experiment didn't work is the difference between __c
and __r
.
A field (any field) that ends in __c
is exactly what we expect it to be. It's simply a field, that holds a single piece of data of a well-defined data type like String
or Decimal
.
The __r
fields are a bit different. When you poke around a little bit, you'll find that a field ending in __r
is not really a field, per-se, but rather an instance of the related object (when you're querying for parent record fields from a child record) or a List
of related object instances (when you perform a parent-child subquery).
You can verify this for yourself by running the following code snippet as anonymous apex
// Standard relationships don't have '__c' and '__r', the corresponding fields
// for standard relationships is usually <objectName>Id and <objectName>, respectively
// e.g. 'AccountId' and 'Account'
List<Opportunity> oppList = [SELECT Id, Name, Account.Name FROM Opportunity LIMIT 10];
Account relatedAccount;
for(Opportunity opp :oppList){
relatedAccount = opp.Account;
system.debug('for opp w/Id: ' + opp.Id);
system.debug('Account Name: ' + relatedAccount.Name);
system.debug(relatedAccount + '\n');
}
When we query for fields on a parent object like Account.Name
, we're telling Salesforce exactly which fields to populate in-memory for the related Account
instances that Salesforce embeds into the Opportunity
instances returned by the query.
The reason why your experiment was returning nulls
In your experiment, you were only populating Object2__c
, the relationship field itself. Because you didn't query for the data, or explicitly populate Object2__r
with an actual instance of Object2__c
, your instance of Object3__c
, newObj
, did not contain (in-memory) any data for Object2__r
(or anything for Object1__c
or Object1__r
).
As the data was not in-memory, you trying to access the data results in null
being returned.
As a side note, one of the things that we take for granted from Salesforce and Apex is the fact that you got null values instead of a null-pointer exception (NPE) from System.debug(newObj.Object2__r.Object1__r);
.
By all rights, that debug statement should throw an NPE, because newObj.Object2__r
evaluates to null, and null.Object1__r
would normally be 'de-referencing' a null value. Salesforce takes care of this for us when using sObject instances.
What you would need to do to get your experiment to work
If you have all the data in memory, then you don't technically need to query for the related data. You'd just need to populate the embedded instance of your related objects.
Object1__c obj1 = new Object1__c(Name = "obj1 Name", SomeField__c = "some value");
Object2__c obj2 = new Object2__c(Name = "obj2 Name", Object1__r = obj1);
Object3__c newObj = new Object3__c(Object2__r=obj2);
System.debug(newObj.Object2__r);
System.debug(newObj.Object2__r.Object1__r);
System.debug(newObj.Object2__r.Object1__r.Some_Field__c);
I hope you can see that this is a cumbersome approach. It is possible, but honestly, you're probably better off just letting a query do the work for you.
Before anyone starts trying to be "clever" with this information...
No, you cannot use this to update parent or child records with a single DML call. Further, this will not get you data for related records in trigger context variables.