5

I have Object1__c and Object2__c which are related by a master-detail relationship (Object1__c is the master).

I want to be able to query a record of Object2__c in such a way that I have access to the parent record as well.

For example, how can I do:

Object2__c obj = [SELECT ... FROM Object1__c WHERE <some clause>][0];
System.debug(obj.Object1__r.Some_Field__c);

Also, I did a little experimentation related to this and found something interesting. Imagine the following hierarchy:

Object1__c
    ^
    | MD relationship
Object2__c
    ^
    | MD relationship
Object3__c

I tried doing this:

Object3__c newObj = new Object3__c(Object2__c=<Id of existing Object2__c record>); 
System.debug(newObj.Object2__r);
System.debug(newObj.Object2__r.Object1__r);
System.debug(newObj.Object2__r.Object1__r.Some_Field__c);

But all I get is:

null
null
null

Why?

4 Answers 4

14

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.

2
  • Nailed it. You need to set or query the name pointing reference in order to access it.
    – Adrian Larson
    Commented Feb 15, 2017 at 15:52
  • +1 for even explaining why Salesforce doesn't throw a null pointer exception. I was scratching my head over that too. Your answer has made me a tad bit wiser to Salesforce's eccentricities. Thanks a ton! Also, yes, my current solution is to insert and then re-query with the Id, at the cost of an SOQL query.
    – coldspeed
    Commented Feb 15, 2017 at 17:22
2

What is your little experimentation, was unable to understand.

You have to query, just instantiating is not sufficient to fetch additional fields.

Object3__c newObj = new Object3__c(Object2__c=<Id of existing Object2__c record>); 
// You are just instantiating the object, for accessing fields you have to query
System.debug(newObj.Object2__r);
System.debug(newObj.Object2__r.Object1__r);
System.debug(newObj.Object2__r.Object1__r.Some_Field__c);

You can query on second object, using __r you would be able to fetch parent fields, for child fields do a inner join. Here is an example:

for(Object2__c obj2: [SELECT Id, Some_Field2__c, Object1__r.Some_Field1__c, 
    (SELECT Id, Some_Field3__c FROM Object3__r) 
    FROM Object2__c]) {

    // Access fields from object1
    System.debug(obj2.Object1__r.Some_Field1__c);

    // Access fields from object2
    System.debug(obj2.Some_Field2__c);

    // query fields from object3
    for(Object3__c obj3: obj2.Object3__r) {
        // Access fields from object3
        System.debug(obj3.Some_Field3__c);
    }
}
4
  • I want to avoid querying the parent again.
    – coldspeed
    Commented Feb 15, 2017 at 13:45
  • If you read the example, the code fetches 3 objects in single query
    – Raul
    Commented Feb 15, 2017 at 13:47
  • What I meant to say was, I wanted a single query instead of an unwieldy loop. I figured out one adhoc solution was to insert the record and then query it again, specifying the relationship fields. But thanks for your help.
    – coldspeed
    Commented Feb 15, 2017 at 14:39
  • adhoc are not good ;)
    – Raul
    Commented Feb 15, 2017 at 14:43
2

Just want to add to Derek's answer, which seems to most directly address your core issue. However my additional perspective a bit too much for a comment.

When you try to get __r data, you're getting a related SObject record. These two statements are essentially equivalent:

Object1__c record = new Object1__c();
SObject parent;

// below lines are equivalent
parent = record.Object2__r;
parent = record.getSObject('Object2__r');

In order for this name pointing reference to be set, you have to either query for it or set it explicitly. Setting the name pointing reference would be:

// below lines are equivalent
record.Object2__r = new Object2__c(/*data*/);
record.putSObject('Object2__r', new Object2__c(/*data*/));

Setting the relationship via Id does not affect this name pointing reference in the least, unless you re-query for the data.

insert new Object1__c(Object2__c = someParentId);
Object1__c record = [SELECT Object2__r.Name FROM Object1__c LIMIT 1];

// now the name pointing reference is populated (via query)
parent = record.Object2__r;
parent = record.getSObject('Object2__r');
1
  • This is exactly what I found out after a little more experimenting. I kind of had a suspicion this was the reason, so thank you for confirming that.
    – coldspeed
    Commented Feb 15, 2017 at 17:18
1

You can use parent to child relationship query all the child records of parent if you know the relationship name.

Understand child relationship names and use that name in inner query. For example:

List<Object1__c> obj = [SELECT Id, (SELECT Id FROM Object2s__r)//Use correct relationship name
                       FROM Object1__c 
                       WHERE <some clause>];

If you need to get parent details from child use dot notation to refer parent fields.

List<Object2__c> obj = [SELECT Id, Object1__c, Object1__r.Name
                        FROM Object2__c 
                        WHERE <some clause>];

The reason you are getting null is you simply initializing Object3__c obj. You need to insert it with object2__c value. object2__c record should insert with object1__c value.

You must log in to answer this question.

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