18-character Record Ids

A while back I created an application to convert 15-character record Ids to their 18-character equivalent. That application is still available. It meets the needs for most admins/developers. Copy the 15 characters into the input field and it will convert to 18. Simple.

What if you want to see the 18-character Id for all records in a report?

Interesting problem. My existing app doesn't do that. And, if you've ever included the record Id in a report, you will notice that it's only a length of 15.

The short answer is create a custom formula field using CASESAFEID(id). Done.

However, for the sake of knowledge sharing, let's look at another less practical solution that uses some Apex logic.

First, we need to understand what it is that we are seeking:

  1. The value must always be populated. No null/blank/empty values on records.
  2. The value must always be accurate. What if we merge records?
  3. The logic should be easy to implement on any object.
  4. The logic should run in the background. Asynchronous. Do not bother or interrupt our Users.

Since all Orgs have Contacts, we will be using that object as our starting point. Create a new field on Contact within Salesforce setup:

  • Object: Contact
  • Field Type: Text
  • Field Label: Id (18)
  • Length: 18
  • Field Name: id18
  • Description: Allows for full record Id to be available in reporting.
  • Help Text: The 18-character version of the record Id.
  • Required: Unchecked
  • Unique: Unchecked
  • External ID: Unchecked
  • Default Value:

We are going to make the new field visible to all Profiles and add it to all page layouts. You should edit the page layouts to make sure the value is read-only.

Now that we have a place to store the full record Id, we need a way to populate it. This will consist of an Apex class and an Apex trigger. Remember our goal is to make something simple and reusable.

The apex class below is our starting point:

/*
    Created by: Greg Hacic
    Last Update: 14 October 2019 by Greg Hacic
    Questions?: greg@interactiveties.com
*/
public class id18Util {
    
    //accepts a map decides whether to process the map further and how to handle that processing (parallel or in-sequence)
    public static void handleMap(Map<Id, String> m) {
        if (!m.isEmpty()) { //if the passed map is not empty
            if (!System.isFuture() && !System.isBatch()) { //if not currently in future or batch context
                handleId18Future(m); //populate the id18__c values asynchronously
            } else { //otherwise
                handleId18(m); //populate the id18__c values synchronously
            }
        }
    }
    
    //accepts a map of Id -> String, determines if the values are different then updates id18__c for those that do not match
    public static void handleId18(Map<Id, String> m) {
        List<SObject> updatedRecords = new List<SObject>(); //list of SObjects
        for (Id i : m.keySet()) { //for each Id key in the map
            Schema.SObjectType t = i.getSObjectType(); //grab the token for this SObject based upon the Id
            Schema.DescribeSObjectResult dsr = t.getDescribe(); //describe the SObject
            Map<String, Schema.SObjectField> fieldMap = dsr.fields.getMap(); //create a map of all of the fields from the describe
            if (fieldMap.containsKey('id18__c')) { //if the map contains a key for id18__c
                String id18Value = m.get(i); //grab the id18__c value passed in the map
                if (String.isBlank(id18Value) || !id18Value.equals(i)) { //if the id18__c value is blank, null or empty '' OR the value is not the same as the Id
                    SObject o = t.newSObject(i); //construct a new sObject of this type, with the specified Id
                    o.put('id18__c', i); //set the id18__c value to the Id
                    updatedRecords.add(o); //add the SObject to the list
                }
            }
        }
        if (!updatedRecords.isEmpty()) { //if the list is not empty
            List<Database.SaveResult> updateResults = Database.update(updatedRecords, false); //update SObject records allow for partial fail
        }
    }
    
    //execute the handleId18 method asynchronously
    @future //future annotation indicates asynchronous execution
    public static void handleId18Future(Map<Id, String> m) {
        handleId18(m); //execute the handleId18 method
    }

}

The class consists of three methods. The first one labeled handleMap allows for two different processing paths based upon the context in which the method is invoked. The second method labeled handleId18 is where the work is performed. The third method simply calls the second method using the @isFuture annotation so that the handleId18 method can be handled asynchronously.

The main thing to recognize about this Apex class is that it only relies upon primitive data types. This means that the logic can be used by any Apex trigger. Not just the Contact object Apex trigger we've built below:

/*
	Created by: Greg Hacic
    Last Update: 14 October 2019 by Greg Hacic
	Questions?: greg@interactiveties.com
	
	Notes:
		- keeps the id18__c value accurate on Contact
*/
trigger id18Contact on Contact(after insert, before update) {
    
    Map<Id, String> idMap = new Map<Id, String>(); //map of Contact.Id -> Contact.id18__c
    
    for (Contact c : Trigger.new) { //for each new Contact record
        idMap.put(c.Id, c.id18__c); //populate the map
    }
    
    id18Util.handleMap(idMap); //determine if there is more to be done

}

The trigger itself is very straightforward. Grab the Id and id18__c values from the Contact and pass that to the Apex class where it can determine what needs to be done next. If the id18__c value is incorrect then it will make sure to correct it. Otherwise, no additional processing is required.

Automated Exchange Rates in Salesforce.com

Reduce Repetitive Tasks, Eliminate Errors & Free Up Your Administrators.

Birthday Reminders for Salesforce.com

It might lead to a sale. Or it might make you feel good.