Salesforce Apex - Get all sObjects with data and sort them topologically
Wed Oct 02 2024 21:41:37 GMT+0000 (Coordinated Universal Time)
Saved by @Justus
/**
* @author Justus van den Berg (jfwberg@gmail.com)
* @date May 2022
* @copyright (c) 2024 Justus van den Berg
* @license MIT (See LICENSE file in the project root)
* @description Execute anonymous script to test a dependency topological
* sort against all sObjects that have data in the org and their
* lookup fields. Sorted based on the required loading order.
* Please note this is just a quickly put together example script
* that can be optimised and customized.
*
* @use case The main use case is to sort sObjects and their relationships to create
* the order of loading during data migrations or data restores
*
* @relatedCode https://www.thiscodeworks.com/salesforce-apex-topological-sort-automatically-define-sobject-data-migration-loading-order/66fdbd5592e2590014ee4528
*
* @blog https://medium.com/@justusvandenberg/programmatically-find-the-order-to-load-salesforce-objects-in-a-data-migration-using-apex-1f65841531fb
*/
// Keep track of all objects that have been described
private Set<Schema.SObjectType> finishedSObjectTypes = new Set<Schema.SObjectType>{};
// Map to auto popuplate missing sObjects
private Set<Schema.SObjectType> missingSObjectTypes = new Set<Schema.SObjectType>{};
// The node dependency map
private Map<Schema.SObjectType, Set<Schema.SObjectType>> nodeDependenciesMap = new Map<Schema.SObjectType,Set<Schema.SObjectType>>();
// List of sObjects, generate this automatically
// Update this value to a custom list if you want to manually specify what sObject you want added
String[] sObjectNames = getSObjectsThatHaveData();
// Execute the main logic
populateSObjectNodes(sObjectNames);
// Populate the dependencies
populateDependencies();
// Convert all the schema classes to strings
Map<Object, Set<Object>> convertedNodeDepencyMap = convertSchemaMapToObjectMap();
// Execute the topological search against all sObjects that have data in them
Set<SortUtil.Node> sortedNodeList = SortUtil.topologicalSort(convertSchemaMapToObjectMap());
// Output the results of the topological search
System.debug(JSON.serializePretty(sortedNodeList,true));
// Output the objects and their related objects (this is to verify the sObjects)
System.debug(JSON.serializePretty(convertedNodeDepencyMap, true));
/**
* @description Method that calls the limits API and gets all objects and their record cound
* Add the records with a count > 0 to the output list
* @return A list of all SObjects that contain records
*/
private String[] getSObjectsThatHaveData(){
// List of the output sObjects
String[] sObjectsWithData = new String[]{};
// Create new request
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(URL.getOrgDomainUrl().toExternalForm() + '/services/data/v61.0/limits/recordCount');
request.setMethod('GET');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setHeader('Authorization', 'Bearer ' + userInfo.getSessionId());
HttpResponse response = http.send(request);
// Check response code
if (response.getStatusCode() != 200) {
throw new StringException(response.getBody());
}
// Populate storage list, note fasted JSON parsing happens when directly going into a constructor instead of creating a variable assignment first
for(Object obj : (Object[]) ((Map<String, Object>) JSON.deserializeUntyped(response.getBody())).get('sObjects')){
Map<String,Object> objMap = (Map<String,Object>) obj;
if( (Integer)objMap.get('count') > 0){
sObjectsWithData.add((String) objMap.get('name'));
}
}
// return the list with data
return sObjectsWithData;
}
/**
* @description Method to popualate the sObject nodes
* @param sObjectName A list of sObject API Names
*/
private void populateSObjectNodes(String[] sObjectNames){
// Create the base map with empty sObject dependency sets
for(String sObjectName : sObjectNames){
// Describe the sObject
Schema.DescribeSObjectResult dsor = ( (SObject) Type.forName('Schema.' + sObjectName).newInstance()).getSObjectType().getDescribe();
// We only want writable and updateable sObjects that have a keyprefix
// otherwise we cannot load the data, so skip otherwise
if(dsor.createable == false || dsor.updateable == false || dsor.keyprefix == null){
continue;
}
// Populate the node dependencies map
nodeDependenciesMap.put(
dsor.sobjecttype,
new Set<Schema.SObjectType>{}
);
}
}
/**
* @description Method to popualate the sObject dependencies recursively
*/
private void populateDependencies(){
// Iterate all the object types in the dependencies map
for(Schema.SObjectType sot : nodeDependenciesMap.keySet()){
// Only describe sObjects once
if(finishedSObjectTypes.contains(sot)){
continue;
}
for(Schema.SObjectField sof : sot.getDescribe().fields.getMap().values()){
// Describe the field
Schema.DescribeFieldResult dfr = sof.getDescribe();
// Skip everything except lookup fields
if(dfr.type != Schema.DisplayType.REFERENCE){
continue;
}
// Get the object describe
Schema.DescribeSObjectResult dsor = dfr.referenceto[0].getDescribe();
// Also for the related describes we only want writable objects
// This filteres out things like history objects and RecordTypes
if(dsor.createable == false || dsor.updateable == false || dsor.keyprefix == null){
continue;
}
// Add the lookup field as a depencency
nodeDependenciesMap.get(sot).add(dsor.sobjecttype);
// Dependency is missing, so add it that we can add it later
if(!nodeDependenciesMap.containsKey(dsor.sobjecttype)){
nodeDependenciesMap.put(dsor.sobjecttype, new Set<Schema.SObjectType>{});
missingSObjectTypes.add(dsor.sobjecttype);
}
// If the the sot has been added so can be removed from the missing SOTs
if(missingSObjectTypes.contains(sot)){
missingSObjectTypes.remove(sot);
}
}
// Flat that sObject describe has finished
finishedSObjectTypes.add(sot);
}
// Recursively call self
if(!missingSObjectTypes.isEmpty()){
populateDependencies();
}
}
/**
* @description Method to convert all Schema.SObjectTypes to Strings
* Fixes the "bug" where the self reference should not
* be the first dependency
* @return A node/depencency map (object,Set<Object>)
*/
private Map<Object,Set<Object>> convertSchemaMapToObjectMap(){
// Output map
Map<Object,Set<Object>> objectMap = new Map<Object,Set<Object>>();
// Add the string values for each sObject
for(Schema.SObjectType sot : nodeDependenciesMap.keySet()){
// Set for holding the dependencies
Set<Object> dependencies = new Set<Object>{};
// Counter
Integer i = 0;
// Indicate if relationship to self needs to be added to the end
// This fixes a bug where if the first dependency is to itself all other get ignored.
Boolean addSelfToEnd = false;
// Convert dependencies
for(Schema.SObjectType dependency : nodeDependenciesMap.get(sot)){
if(i==0 && (dependency == sot)){
addSelfToEnd = true;
i++;
continue;
}
dependencies.add(String.valueOf(dependency));
i++;
}
// Add self to end of dependencies
if(addSelfToEnd){
dependencies.add(String.valueOf(sot));
addSelfToEnd = false;
}
// Add the object type and dependencies
objectMap.put(String.valueOf(sot),dependencies);
}
// Return the converted map
return objectMap;
}



Comments