Thursday, May 12, 2016

Summer '16 new Apex Method - Get a Map of Populated SObject Fields

I've just found my new favorite Apex method in the Summer '16 Release notes - Get a Map of Populated SObject Fields

// In Summer ’16, we’ve introduced a new method on the SObject class that returns a map of populated field names and their corresponding values:
Map<String, Object> getPopulatedFieldsAsMap()

Where is this immediately helpful? Maybe this error message looks familiar:

System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Account.Name

In the simplest case it comes from something like the following:

List accs = [Select Id from Account];
System.debug(accs[0].Name);

The Accounts were queried for just the Id field, and then the code immediately tries to access the Name field of an Account, which wasn't queried. The SOQL query needs to be expanded to something like: Select Id, Name from Account. In an ideal world fixing this would be as simple as back tracking a few lines of code to find where the Account was queried and then adding the missing fields. In practice the "back tracking" may not be all that simple. From when the object was first queried to when it reached the code that needs a specific field to be populated it could have traversed several methods and classes. Possibly even gone through a namespace change or passed off to an unknown class that implements an interface via Type.forName() and Type.newInstance().

With the new Summer '16 sObject method we can do some explicit code checks to see if the random Account instance we've been passed has the expected fields populated.

List<account> accs = [Select Id from Account];
System.assert(accs[0].getPopulatedFieldsAsMap().ContainsKey('Name'), 'The Account should be queried with the Name field');
System.debug(accs[0].Name);

This will also work for sObjects that haven't been inserted yet. You can see which fields have been populated.

Contact con = new Contact();
con.firstname = 'John';
System.assert(con.getPopulatedFieldsAsMap().ContainsKey('LastName'), 'The Contact should have a LastName');

In practice you would likely store the Map that comes out of getPopulatedFieldsAsMap() in a variable and do numerous checks on it.

Having an assertion fail isn't the most elegant solution. Not much better than the existing SObjectException. So how do you handle a missing field?

You could go back to the originating SOQL query and add the missing field. However, as mentioned above, you may have little or no control of that query. Instead, you could build up a dynamic SOQL query that will pull just the identified missing fields for all the sObjects affected. Then use the sObject.put method to merge the results back into the original sObjects.

Contact con = [Select Id, FirstName from Contact limit 1]; // Whoops, forgot to query the LastName!

List<string> requiredFields = new List<string> {'FirstName', 'LastName'};

string dynamicQuery = 'Select Id';
boolean queryRequired = false;
Map<String, Object> fieldsToValue = con.getPopulatedFieldsAsMap();
for(string requiredField : requiredFields) {
    if(!fieldsToValue.containsKey(requiredField)) {
        dynamicQuery += ', '+ requiredField;
        queryRequired = true;
    }
}

if(queryRequired) {
    // Exercise for the reader, better bulkificaiton support for dealing with multiple sObjects
    dynamicQuery += ' From Contact where Id in (\''+con.Id+'\')';
    for(Contact mergingContact : Database.query(dynamicQuery)) {
        // TODO: Matchup queried Contacts with base contacts on Id (or an external Id)
        Map<String, Object> mergingFieldsToValue = mergingContact.getPopulatedFieldsAsMap();
        for(string fieldName : mergingFieldsToValue.keySet()) {
            con.put(fieldName, mergingFieldsToValue.get(fieldName));
        }        
    }
}

System.debug(con); //12:55:50:007 USER_DEBUG [12]|DEBUG|Contact:{FirstName=John, LastName=Doe}

Things that would make it even more useful:

  • Being able to "depopulate" a field on an sObject. E.g. you've got an Account instance, but only want send specific fields in for a DML update. Currently you would need to create a new sObject and set just the fields you wanted. If you could depopulate them instead you could use the original instance without the risk of updating fields that should be unchanged.
  • A built in way to retrieve additional fields into a collection of sObjects. My sample implementation above should work in theory, but it would be great to provide a collection of sObjects and the names of the additional fields you need populated and have Apex do the rest.
        List<Contact> someListOfContactsWithoutFirstName = //...
        List<string> additionalFields = new List<string> {'FirstName'};
        List<Contact> awesomeListOfContactsNowWithFirstName = Database.queryFields(someListOfContactsWithoutFirstName, additionalFields);
    

Other Summer '16 highlights via Summer '16 Highlights for ISV Developers

See also: