Thursday, May 19, 2016

Trailhead - Custom Metadata Types

When Custom Metadata Types were first introduced my first reaction was - What is the difference between Custom Settings and Custom Metadata Types?. Why would you use one over the other?

There is now a convenient new Trailhead module that helps answer this question with gif animations in the first unit.

The key point from these gifs are the configuration records that represent the actual configuration. If you are currently using custom settings or custom objects to hold configuration for your org, it's well worth exploring how Custom Metadata Types can help with deployments.

Other useful areas of Custom Metadata Types you can explore in the module:

  • You can use the Custom Metadata Loader to bulk load up to 200 custom metadata records from a CSV.
  • How to configure new Custom Metadata Types and the corresponding records.
  • How custom metadata records are accessed in testing contexts.
  • How to control access to the Custom Metadata Types and fields
  • How to include both the Custom Metadata Type and the corresponding records in a package.
  • How to convert from list custom settings to custom metadata types.
  • There is also some wisdom hidden away in the challenge questions (although it may not be the answer Trailhead is looking for):

    See also:

    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: