1

This is a question about the isSet function on SObjects. Compare a similar question from 2019 here, but the discussion did not go beyond OwnerId.

In my case, Apex gets a QuoteLineItem from a query (in reality dynamic SOQL) like

QuoteLineItem qli = [SELECT Product2Id FROM QuoteLineItem LIMIT 1];

I want to conditionally avoid execution of the following line (and a bunch of other references to Product fields) to avoid "System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: QuoteLineItem.Product2":

String s = qli.Product2.Name;

For a custom child object of Product2 the following isSet returns true...

ProductChild__c child = [SELECT Product__r.Name FROM ProductChild__c LIMIT 1];
System.assert(child.isSet(ProductChild__c.Product__c));

... and it returns false, when the lookup is not in the SELECT.

But is isSet of any use in my case?

I've tried different flavors of isSet for the Product2.Name reference:

// field version
System.assert(qli.isSet(QuoteLineItem.Product2Id)); // passes, but unhelpful
System.assert(qli.isSet(QuoteLineItem.Product2.Id)); // passes, but unhelpful
System.assert(qli.isSet(QuoteLineItem.Product2.Name)); // does not save to Salesforce
System.assert(qli.isSet(QuoteLineItem.Product2)); // does not save to Salesforce

// String version
System.assert(qli.isSet('Product2Id')); // passes, but unhelpful
System.assert(qli.isSet('Product2.Id')); // System.SObjectException: Invalid field: Product2.Id
System.assert(qli.isSet('Product2.Name')); // System.SObjectException: Invalid field: Product2.Name
System.assert(qli.isSet('Product2')); // System.SObjectException: Invalid field: Product2

(This shows for a trigger context isSet returns more often false than intuitive, but above tests never see false anyway.)

Is there a correct form?

2
  • Is the issue that you are trying to get it to work cross-object? I believe it should only work on fields on the object itself.
    – Adrian Larson
    Oct 17, 2022 at 15:13
  • It works for regular lookup fields e.g. on custom objects, see the ProductChild__c example. Or have I missed something? Oct 17, 2022 at 15:15

1 Answer 1

3

tl;dr: No, isSet won't help here, because it only works on fields on the object. There are a few tricks you can use, though.


QuoteLineItem.Product2.Name and QuoteLineItem.Product2 don't work because they're not real fields, they're relationships, which is why they don't compile. QuoteLineItem.Product2Id works because it's a sObjectField token, a static field reference autogenerated by Salesforce.

Also, there's a peculiar oddness regarding sObjects themselves: they allow traversal across null objects without any errors:

Contact c = new Contact();
System.debug(c.Account.Owner.Manager.Name); // null

These two features combined means that there's direct way to detect this problem ahead of time.

There are two possible workarounds.

Option 1 is to JSON serialize and deserialize:

QuoteLineItem qli = [SELECT Product2Id FROM QuoteLineItem LIMIT 1];
qli = (QuoteLineItem)JSON.deserialize(JSON.serialize(qli), QuoteLineItem.class);
// This should fail now.
System.assertNotEquals(false, qli.Product2?.isSet(Opportunity.Name) != null); 

That's a major CPU hog on large objects, so I'd avoid this if possible. Note that merely using the clone() or deepClone methods (for lists) aren't enough. You have to actually go through JSON to remove the "retrieved from SOQL" internal flag that throws this exception.

Option 2 is to test it ahead of time:

Boolean hasProductData = true;
try {
  String productName = qli.Product2.Name;
} catch(sObjectException e) {
  hasProductData = false;
}
if(hasProductData) {
  ...

For reusability, you can write a method:

public static Boolean isSet(sObject record, sObjectField[] path) {
  sObject temp = record;
  sObjectField[] tempPath = path.clone();
  while(tempPath.size() > 1 && temp != null) {
    try {
      temp = record.getSObject(tempPath.remove(0));
    } catch(sObjectException e) {
      return false;
    }
  }
  if(tempPath.size() > 1) {
    return true;
  }
  try {
    record.get(tempPath[0]);
    return true;
  } catch(sObjectException e) {
    return false;
  }
}

Note that a null value could trick this algorithm into being incorrect, so be wary of using this to detect nillable fields, perhaps with a try-catch in the main logic that calls this as well).


The non-detectability of this problem beyond try-catching it is a rather annoying limitation. In practice, it would be better to make sure your queries are always including the correct fields. Avoid making queries that won't include such rows. Doing so has the peculiar code smell odor we programmers try to avoid. This is true in every other database system, and it applies equally here.

3
  • Many thanks! Like always - ask 1 question and learn 10 things! Once again I can't believe how quickly you write such things up. "it only works on fields on the object" - it's frustrating isSet works on all sorts of lookups, but not this one - is this a Salesforce legacy?! "make sure your queries are ... including the correct fields" - this is dynamic SOQL fetching all fields of the QuoteLineItems (via SObjectType.getDescribe().fields.getMap()) in order to copy a (configurable) subset to another object. "Avoid making queries that won't include such rows..." - do you mean columns?! Oct 17, 2022 at 16:31
  • @FelixvanHove it works on the actual ID fields, but not their relationships (parent or child). IsSet is relatively new. I did fix the odd wording. And, at least on a PC, I type up to 100 wpm on a good day. 😀
    – sfdcfox
    Oct 17, 2022 at 16:44
  • "I did fix the odd wording" - I don't see any edits (not sure, if you can do invisible edits), but in particular not regarding anything I asked, citing from your text. Oct 17, 2022 at 17:12

You must log in to answer this question.

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