Upserting and Working with Custom Metadata Types
Discussion
Previously, we discussed how to use Custom Metadata Types in conjunction with generic sObjects to provide a fully customized implementation specific to a given customer.
But now, let's suppose you want the ability to Insert or Update your Custom Metadata Types through code. After all, you could do that with Custom Settings, so shouldn't you be able to do so with Custom Metadata Types?
With Salesforce API Version 40, the answer is now YES!
Upserting Custom Metadata Type Records
The first thing to understand about this solution is that Upserting these records is a "queueable" process. That is, it happens asynchronously, so you won't be able to wait for the answer before using the value. If that is a requirement, you'll need to use Custom Settings or another solution.
Another small limitation is that there is a limit at this time of 50 "queueable" events. So, because of this, if you have a number of records to Upsert, you're going to want to use a LIST collection method of doing so. And, after all, bulk processing is what Salesforce is all about, right?
And why do I keep saying "Upsert"? Because you can't determine via the API whether something is an Insert or an Update. The DeveloperName that you pass as part of the call makes that decision for you; much as an External ID does for a normal sObject Upsert.
Finally, it may seem obvious, but the values you will assign to a given field could be many different types. String, numeric, datetime, or their variants (email, phone, picklist, etc). Therefore, remember that what you are actually working with when Upserting will be the "Object" datatype. Not sObject, but Object!
The Solution Discussion
The solution to this is below. But just a few notes.
First, there are 2 overload methods of "Upsert_MDT_Value". One for a single value, and one for a LIST of values.
Since we are going to have a number of methods, let's encapsulate them in a dedicated class so that it can be consumed by a number of places. I've called mine "CustomMetadataType_Utility". In addition, I've added methods called getInstance (2 overloads; 1 for the record to be returned and 1 for the field value), getValues (same overloads), and getAll (to return all values for a given Custom Metadata Type). If these look familiar, they should because they mirror how you call Custom Settings. This is quite intentional so that it makes it easy to convert existing work to the new Custom Metadata Type when desired and to work with them.
At the top of the Utility, you'll see a public MAP called cmdtMAP. It is public, though it doesn't need to be so. The purpose isn't necessarily for the caller to interact with it (though they certainly may do so). Rather, it's for the Class to store calls into so that if it is called again, it simply returns the value it already stored for efficiency.
The 2 overloads of getInstance and getValues are to be like Custom Settings. One to return an sObject representation of the Custom Metadata Type Record matching the request (which of course can be cast into the desired CMDT), and one to return a string representation of any desired field to make the calls more succinct and easier to convert from Custom Settings if desired.
Finally, the 2 overloads of Upsert_MDT_Value. The first is for a single value, and the second is for a LIST of values. As mentioned above, if you want more than a single value to Upserted, use the LIST because of the limit on "queueable" events.
A quick discussion on the methods. The first takes 4 parameters, while the second takes 1 parameter. But the single parameter is a LIST of a sub-class in the Utility class called "CustomMetadataType_DataType". The 1st parameter for either is easy enough and is the applicable desired Custom Metadata Type object and record separated by a period (i.e. MDT_Key_IDs__mdt.Integration).
The example shown says go to the CMDT object MDT_Key_IDs__mdt, and Upsert a Record (DeveloperName) called Integration. That is your matching criteria. Once the record defined, we need to tell it the MasterLabel; what shows on screen for the record. Currently, the API isn't smart enough to know it's an update so the MasterLabel isn't required, so it must always be provided. Once that is done, you need to provide a list of fields to update. This can be a single field, or it can be multiple fields. BUT, the next parameter is the list of OBJECT VALUES to assign to them. The lists must be of the same size, or the Upsert will not take place.
Great! Let's see some examples. First, let's simply get the entire record for a given value. Simple:
getInstance CMDT Sample
MDT_Key_IDs__mdt mdt = (MDT_Key_IDs__mdt) CustomMetadataType_Utility.getInstance(new MDT_Key_IDs__mdt(), 'CTXS_Integration');
OR
MDT_Key_IDs__mdt mdt = (MDT_Key_IDs__mdt) CustomMetadataType_Utility.getValues(new MDT_Key_IDs__mdt(), 'CTXS_Integration');
Remember, getInstance and getValues are exactly the same return; just like with custom settings. But now, let's get the value for a single field:
getInstance CMDT Field Sample
string strID = CustomMetadataType_Utility.getInstance(new MDT_Key_IDs__mdt(), 'CTXS_Integration', 'Record_ID__c');
OR
string strID = CustomMetadataType_Utility.getValues(new MDT_Key_IDs__mdt(), 'CTXS_Integration', 'Record_ID__c');
But suppose we want a MAP of all of the values like we can for a Custom Setting?
getAll CMDT Sample
MAP<string, sObject> keyIDMAP = CustomMetadataType_Utility.getAll(new MDT_Key_IDs__mdt());
(which can can by hard cast like)
MAP<string, MDT_Key_IDs__mdt> keyIDMAP = (MAP<string, MDT_Key_IDs__mdt>) CustomMetadataType_Utility.getAll(new MDT_Key_IDs__mdt());
But, now suppose I want to update a value for a record to show the last time processed and the name of the person who processed it. I could use the below:
Upsert Single CMDT Value Sample
CustomMetadataType_Utility.Upsert_MDT_Value('MDT_My_CMDT__mdt.My_Batch_Settings',
'Settings',
new LIST<string>{'Processed_By__c', 'Last_Processed__c'},
new LIST<object>{UserInfo.getUserID(), system.now()}
);
Pretty simple, huh? But, suppose I've got a group of values to Upsert. Don't want to do those in a loop for the reasons previously discussed. So, we'll:
Upsert CMDT Values LIST Sample
IST<CustomMetadataType_Utility.CustomMetadataType_DataType> cmdtDTLIST = new LIST<CustomMetadataType_Utility.CustomMetadataType_DataType>();
CustomMetadataType_Utility.CustomMetadataType_DataType cmdtDT = new CustomMetadataType_Utility.CustomMetadataType_DataType();
cmdtDT = new CustomMetadataType_Utility.CustomMetadataType_DataType();
cmdtDT.strMDTAndRecord = 'MDT_My_CMDT__mdt.Integration_Data';
cmdtDT.strMasterLabel = 'Integration Data';
cmdtDT.fieldLIST = new LIST<string>{'ProfileName__c', 'NbrProcessed__c'};
cmdtDT.valueLIST = new LIST<object>{'CTXS_Integration', nNbrProcessed};
cmdtDTLIST.add(cmdtDT);
cmdtDT = new CustomMetadataType_Utility.CustomMetadataType_DataType();
cmdtDT.strMDTAndRecord = 'MDT_My_CMDT__mdt.System_Administrator_Data';
cmdtDT.strMasterLabel = 'System Administrator Data';
cmdtDT.fieldLIST = new LIST<string>{'ProfileName__c', 'NbrProcessed__c'};
cmdtDT.valueLIST = new LIST<object>{'CTXS_System Administration', nNbrProcessed};
cmdtDTLIST.add(cmdtDT);
CustomMetadataType_Utility.Upsert_MDT_Value(cmdtDTLIST);
Note the small change here is that we used the LIST overload, and used the sub-class to define each of those list items. Otherwise, the behavior is the same.
Conclusion
Working with Custom Metadata Types need not be so confusing and complex. With some small encapsulations like the above, you can now call the data in a single line and concentrate on your actual solution without the distraction of working with them.
I hope this has been beneficial and I welcome any comments or feedback.
The Solution!
The final solution is below. There is a call to a "Utility" method you'll see called sObjectQuery. All it does is dynamically create the query for you and return an sObject. The code is below if you want to use it, but for simple tasks, it's really unnecessary.
public static LIST<sObject>sObjectQuery(sObject sObj, string strFields, string strSubSelect, string strIDs, string strFilter, string strOrderBy, integer nLimit, datetime dtSince) {
/*
Created By: Robert Nunemaker
Created On: 01/03/2013
Purpose: Return a query of an sObject with optional fields, filters, and other parameters.
Parameters:
PARAMETER OPTIONAL/MANDATORY TYPE DEFAULT DESCRIPTION
sObj Mandatory sObject NONE Provide an example sObject (i.e. (sObject) new Lead()) for the query to be based on.
dtSince Optional string 2000-01-01 00:00:00 Provide the desired date/time in standard SalesForce format (i.e. 2012-12-26 00:07:00 in the EST timezone) to provide a date/time filter. Becomes part of the Where clause.
strOrderBy Optional string NULL Provide a string Field list separated by a comma for the order to return records.
strFields Optional string All Provide a string Field list separated by a comma for the order to return only those fields.
strSubSelect Optional string NULL Provide a string of the Subselect to include in fields including the necessary open/close parentheses (i.e. (Select ID From Contacts))
strIDs Optional string NULL Provide a string Field list separated by a comma for the ID's to return. Becomes part of the WHERE clause.
strFilter Optional string NULL Provide a "Where" clause (without the Where) of the conditions to apply to the query.
nLIMIT Optional number Unlimited Provide the max number of records to return if desired.
--------------------------------------------------------------------------
Modified By:
Modified On:
Modification:
*/
LIST<string>strIDLIST = new LIST<string>();
LIST<string>whereClauseLIST = new LIST<string>();
string strSOQL = '';
string strObjName = string.valueOf(sObj.getsObjectType());
if (strFields<>null && strFields<>'') {
strSOQL = 'SELECT ' + strFields;
} else {
// Build SOQL Field string with all fields accessible by the user
DynamicQueryBuilder dqBuilder = new DynamicQueryBuilder(sObj);
SET<string>fldToIgnoreSET = new SET<string>();
SET<string>strBogusFilterSET = new SET<string>();
strBogusFilterSET.add('xxx');
string strFieldList = dqBuilder.getAllFldQueryString(strBogusFilterSET, 'ID');
// Now strip the From Clause
integer nCol = strFieldList.indexOf(' from ');
strSOQL = strFieldList.left(nCol);
} // END if (strFields <> null && strFields <> '')
if (strSubSelect<>null && strSubSelect<>'') {
strSOQL += ', ' + strSubSelect;
} // END if (strSubSelect <> null && strSubSelect <> '')
strSOQL += ' FROM ' + strObjName;
if (dtSince<>null) {
whereClauseLIST.add('CreatedDate >= :dtSince');
} // END if (dtSince <> null)
//WHERE CreatedDate >= :dtSince ';
if (strFilter<>null && strFilter<>'') {
//strSOQL += strFilter;
whereClauseLIST.add('(' + strFilter + ')');
} // END if (strFilter <> null && strFilter <> '')
if (strIDs<>null && strIDs<>'') {
strIDLIST = strIDs.split(',');
whereClauseLIST.add('ID in :strIDLIST');
} // END if (strIDs <> null && strIDs <> '')
integer nIndex = 0;
for (string strValue: whereClauseLIST) {
if (nIndex == 0) {
strSOQL += ' WHERE ' + strValue;
} else {
strSOQL += ' AND ' + strValue;
} // END if (nIndex == 0)
nIndex++;
} // END for (sObject sObj : whereClauseLIST)
if (strOrderBy<>null && strOrderBy<>'') {
strSOQL += ' Order By ' + strOrderBy;
} // END if (strOrderBy <> null && strOrderBy <> '')
if (nLimit<>null && nLimit>0) {
strSOQL += ' LIMIT ' + string.valueOf(nLimit);
} // END if (nLimit <> null && nLimit > 0)
system.debug('Processing ' + strObjName + ' request for SOQL: ' + strSOQL);
return database.query(strSOQL);
} // END sObjectQuery
public class CustomMetadataType_Utility {
/*
Created By: Robert Nunemaker
Created On: 07/31/2017
Purpose: Utility methods to get and work with Custom Metadata Types
--------------------------------------------------------------------------
Modified By:
Modified On:
Modification:
*/
public static MAP<sObject, MAP<string, sObject>> cmdtMAP = new MAP<sObject, MAP<string, sObject>>();
public static MAP<string, sObject> getAll(sObject sObj) {
MAP<string, sObject> CMDTMAP = new MAP<string, sObject>();
for (sObject sObjItem : Utility.sObjectQuery(sObj, null, null, null, null, null, null, null)) {
CMDTMAP.put((string) sObjItem.get('Label'), sObjItem);
} // END for (sObject sObjItem : Utility.sObjectQuery(sObj, null, null, null, null, null, null, null))
return CMDTMAP;
} // END getAll
public static sObject getInstance(sObject sObj, string strRecordName) {
if (!cmdtMAP.containsKey(sObj) || cmdtMAP.get(sObj).size() == 0) cmdtMAP = loadMAP(sObj);
sObject sObjReturn = null;
if (cmdtMAP.containsKey(sObj) && cmdtMAP.get(sObj).containsKey(strRecordName.toLowerCase())) sObjReturn = cmdtMAP.get(sObj).get(strRecordName.toLowerCase());
return sObjReturn;
} // END getInstance
public static string getInstance(sObject sObj, string strRecordName, string strFieldName) {
sObject sObjReturn = getInstance(sObj, strRecordName);
system.debug('sObjReturn: ' + sObjReturn);
return string.valueOf(sObjReturn.get(strFieldName));
} // END getInstance
public static sObject getValues(sObject sObj, string strRecordName) {
return getInstance(sObj, strRecordName);
} // END getValues
public static string getValues(sObject sObj, string strRecordName, string strFieldName) {
sObject sObjReturn = getInstance(sObj, strRecordName);
system.debug('sObjReturn: ' + sObjReturn);
return string.valueOf(sObjReturn.get(strFieldName));
} // END getValues
private static MAP<sObject, MAP<string, sObject>> loadMAP(sObject sObj) {
MAP<sObject, MAP<string, sObject>> returnMAP = cmdtMAP;
MAP<string, sObject> mapItems = new MAP<string, sObject>();
for (sObject sObjItem : Utility.sObjectQuery(sObj, null, null, null, null, null, null, null)) {
mapItems.put(((string) sObjItem.get('Label')).toLowerCase(), sObjItem);
} // END for (sObject sObjItem : Utility.sObjectQuery(sObj, null, null, null, null, null, null, null))
returnMAP.put(sObj, mapItems);
system.debug('Returning loaded MAP of Custom Metadata Type records: ' + returnMAP);
for (sObject key : returnMAP.keySet()) {
MAP<string, sObject> tempMap = returnMAP.get(key);
for (String k : tempMap.keySet())
{
system.debug('key: ' + k + ' val: ' + tempMap.get(k));
}
}
return returnMAP;
} // END loadMAP
public static void Upsert_MDT_Value(string strMDTAndRecord, string strMasterLabel, LIST<string> fieldLIST, LIST<object> valueLIST) {
// parameter passed should be the MDT.DeveloperName (i.e. MDT_Case_Webservice__mdt.Default_Get_SOQL)
// If this is an update to an existing record, only send the applicable field(s) to be updated
// Setup custom metadata to be created
Metadata.CustomMetadata customMetadata = new Metadata.CustomMetadata();
customMetadata.fullName = strMDTAndRecord;
customMetadata.label = strMasterLabel;
Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue();
integer nIndex = 0;
for (string strField : fieldLIST) {
customField = new Metadata.CustomMetadataValue();
customField.field = strField;
customField.value = valueLIST[nIndex];
customMetadata.values.add(customField);
system.debug(customField);
nIndex ++;
} // END for (string strField : fieldLIST)
Metadata.DeployContainer mdContainer = new Metadata.DeployContainer();
mdContainer.addMetadata(customMetadata);
// Setup deploy callback, MyDeployCallback implements
// the Metadata.DeployCallback interface (code for
// this class not shown in this example)
MetadataDeploy_Callback callback = new MetadataDeploy_Callback();
// Enqueue custom metadata deployment
system.debug('Container: ' + mdContainer);
if (!test.isRunningTest()) Id jobId = Metadata.Operations.enqueueDeployment(mdContainer, callback);
} // END Upsert_MDT_Value
public static void Upsert_MDT_Value(LIST<CustomMetadataType_DataType> CMDTLIST) {
// parameter passed should be a LIST of Custom Metadata Type Datatype with the MDT.DeveloperName (i.e. MDT_Case_Webservice__mdt.Default_Get_SOQL)
// If this is an update to an existing record, only send the applicable field(s) to be updated
Metadata.DeployContainer mdContainer = new Metadata.DeployContainer();
Metadata.CustomMetadata customMetadata = new Metadata.CustomMetadata();
Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue();
integer nIndex = 0;
for (CustomMetadataType_DataType cmdtItem : CMDTLIST) {
customMetadata = new Metadata.CustomMetadata();
customMetadata.fullName = cmdtItem.strMDTAndRecord;
customMetadata.label = cmdtItem.strMasterLabel;
nIndex = 0;
for (string strField : cmdtItem.fieldLIST) {
customField = new Metadata.CustomMetadataValue();
customField.field = strField;
customField.value = cmdtItem.valueLIST[nIndex];
customMetadata.values.add(customField);
system.debug(customField);
nIndex ++;
} // END for (string strField : fieldLIST)
system.debug(customMetadata);
mdContainer.addMetadata(customMetadata);
} // END for (CustomMetadataType_DataType cmdtItem : CMDTLIST) {
// Setup deploy callback, MyDeployCallback implements
// the Metadata.DeployCallback interface (code for
// this class not shown in this example)
MetadataDeploy_Callback callback = new MetadataDeploy_Callback();
// Enqueue custom metadata deployment
system.debug('Container: ' + mdContainer);
if (!test.isRunningTest()) Id jobId = Metadata.Operations.enqueueDeployment(mdContainer, callback);
} // END Upsert_MDT_Value
public class MetadataDeploy_Callback implements Metadata.DeployCallback {
public void handleResult(Metadata.DeployResult result, Metadata.DeployCallbackContext callbackContext) {
// Intentionally empty
system.debug(result);
system.debug(callbackContext);
} // END handleResult
} // END MetadataDeploy_Callback
public class CustomMetadataType_DataType {
/*
Created By: Robert Nunemaker
Created On: 07/31/2017
Purpose: DataType to store Custom Metadata Types for APEX upload in bulk
--------------------------------------------------------------------------
Modified By:
Modified On:
Modification:
*/
public string strMDTAndRecord;
public string strMasterLabel;
public LIST<string> fieldLIST = new LIST<string>();
public LIST<object> valueLIST = new LIST<object>();
} // END CustomMetadataType_DataType
} // END CustomMetadataType_Utility
It has been updated. Go to "The Solution" section to see the code.
Actually, it won't let me post that. Too long. I'll put it as a code snippet in an update to the blog.
Yes, it is. Sorry. Didn't catch that. The code is is in the next reply, but essentially, it's just receiving the dynamic fields to create the query and return the object.
This is awesome... however, "Utility.sObjectQuery" is undefined -- is this another utility that you have provided? I couldn't find reference to it here. Robert Nunemaker