10

Background: Long story short, my company is integrating with a platform that has a "Role Hierarchy", so to speak, but that Hierarchy isn't stored in Salesforce. To get the "roles" I have to query their API. Even further than that, each Role can have child Roles, but the only way to find that out is to individually query each Role via their API to see if it has any children. Those children can have children, and those children can have children, etc. In total, with our current setup, it is possible that the parent-child roles could go as far as 6 levels deep.

SO, I think I need to use a 6 level deep nested Map of Strings, like this:

Map<String,Map<String,Map<String,Map<String,Map<String,Map<String,String>>>>>> allLevels = new Map<String,Map<String,Map<String,Map<String,Map<String,Map<String,String>>>>>>();

First question: Is this even possible? Am I exceeding any limits with a Map like this? I have looked over documentation and can't find anything regarding nested map limits.


When I am querying their API, it looks like this:

https://api.website.com/v1/teams?apikey=API-KEY-HERE&source=Salesforce-GETTeams

This returns JSON results like this:

{
    "Id": "pAFwnPpqJTo1",
    "Name": "External",
    "TeamCodeForBulkImport": "446878-External"
},
{
    "Id": "R7tu6KPYk6k1",
    "Name": "English",
    "TeamCodeForBulkImport": "446885-English"
},
{
    "Id": "-7tu-ZPY3Ik1",
    "Name": "Spanish",
    "TeamCodeForBulkImport": "446885-Spanish"
},
{
    "Id": "-TwdJwJzjbY1",
    "Name": "Application",
    "TeamCodeForBulkImport": "446904-Application"
},
{
    "Id": "-Twdzz-jbY1",
    "Name": "Enterprise",
    "TeamCodeForBulkImport": "446904-Enterprise"
},
{
    "Id": "-TWd2w-fXY1",
    "Name": "Enterprise",
    "TeamCodeForBulkImport": "463321-Enterprise"
},

It returns ALL teams, from ALL levels (top level, children, grandchildren, etc) using this query. Note that Enterprise is listed multiple times, that is because Enterprise exists more than once as a child to different parents. In my case, I need to know the true hierarchy which looks something like this:

              v-- External --v
        v-- Spanish        English --v
    v-- Application            Application --v
v-- Enterprise                     Enterprise --v

To get the actual children, I need to do a separate API call using the ID of the Parent, like this:

https://api.website.com/v1/teams/TEAM-ID-HERE/teams?apikey=API-KEY-HERE&source=Salesforce-GETTeams

This will return JSON of ONLY the children of whatever TEAM-ID I specified in my call. So the first thing I want to do is make the "top level" categories (Internal/External) the first String in my map, then loop through those IDs to get the children of each and store them in the "second level" of my String Map, then loop through those SECOND LEVEL IDs and get the children of them and store them in the "third level" of my String map, etc etc, until I've traversed all 6 levels and have all of the data stored in my map.

My goal here is to have a Map where the primary keySet is Internal or External, and then the second keySet is the children of Internal or External, and the third keySet is the children of the second keyset, and so on until I've reached the end. My result should be a very elaborate map that I can then use in combination with data sourced from Contact records to determine which group to assign them to, and get that groups ID from this nested map.

Second question: Since I will be storing data incrementally in the map (as detailed above), I need to store data in the map as I get it, and continue to build the map as I go through the loops and make the calls. I've never dealt with a nested map before, especially not a 6-level-deep nested map. How would I properly insert data into the map for the second level while maintaining the third, fourth, fifth, and sixth levels as null, and then move on to storing data in the third level while keeping the fourth, fifth, and sixth null, and so on, until the map is complete?

--- FINAL WORKING CODE ---

After a back-and-forth with sfdcfox, I ended up creating a wrapper class that contained the String id,parentid,name; of the CustomRole. It also contained a Map<String,CustomRole> childRoles. I start by doing an API call to get ALL roles and store them in Map<String,CustomRole> roleMap, then I loop through the values in roleMap to do the individual sub-team API calls, and then I pull the current Role's childRoles map from the roleMap, and add to it for each child role I get back in my sub-team API call.

This results in a Map<String,CustomRole> where I can loop through the main role and then do an inner loop for its childRoles.

public with sharing class FutureCaller {
    private static final String apikey = 'xxxxx-xx-x-xxxx-x';
    private static final String endpoint = 'https://api.website.com/v1.svc';

    private LitmosFutureCaller() {}

    public class CustomRole{ 
        public String guid, parentId, name; 
        public Map<String, CustomRole> childRoles; 

        public CustomRole(String teamId,String parentId,String name){
            this.guid = teamId;
            this.parentId = parentId;
            this.name = name;
            this.childRoles = new Map<String,CustomRole>();
        }
    }

    @future (callout=true)
    public static void getTeams(Set<Id> contactIds, Boolean isNew){
        List<Contact> newList = [SELECT Id,Primary_Language__c,Activated__c,LoginKey__c,lmsID__c,Title__c FROM Contact WHERE ID in :contactIds];

        // create our roleMap to populate with role Id,CustomRole class
        Map<String, CustomRole> roleMap = new Map<String, CustomRole>();
        // create source string for use in API calls
        String source; 
        // Strings to hold Id and Name for each object in the parser
        String teamId;
        String teamName;

        // set source string for use in API call
        source = 'MSI-Apex-getTeams';

        // build HTTP GET Request with JSON Return
        HttpRequest httpReq = new HttpRequest();
        httpReq.setMethod('GET');
        httpReq.setHeader('Accept','application/json');
        httpReq.setEndpoint(endpoint+'/teams?apikey='+apikey+'&source='+source);

        // create HTTP for Callout
        Http http = new Http();

        // first we need to build the roleMap by making an initial API call
        // to get all teams(roles). Their Id will be the key, and the 
        // CustomRole wrapper will be the value
        try{
            // Get HTTP Callout Response
            HTTPResponse httpResp = http.send(httpReq);

            // Create Parser
            JSONParser parser = JSON.createParser(httpResp.getBody());
            while(parser.nextToken() != null){
                // if Current Token is the Id field, go to Next Token and set teamId
                if((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
                    (parser.getText() == 'Id')){
                        parser.nextToken();
                        teamId = parser.getText();
                    }
                // if Current Token is the Name field, go to Next Token and set teamName
                if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && 
                   (parser.getText() == 'Name')){
                        parser.nextToken();
                        teamName = parser.getText();       
                   }
                // once we reach the end of the object...
                if((parser.getCurrentToken() == JSONToken.END_OBJECT)){
                    // If the roleMap doesn't contain the key (teamId)
                    if(!roleMap.containsKey(teamId)){
                        // add the related Id to the String,CustomRole(guid,parentId,name) map
                        roleMap.put(teamId,new CustomRole(teamId,null,teamName));
                    }

                    // then reset the team Id and team Name to null before next object in the parser
                    teamId = null;
                    teamName = null;   
                }
            }
            System.debug('Final Top Level KeySet: ' + roleMap.keySet());
            System.debug('Final Top Level Values: ' + roleMap.values());
        }catch(System.CalloutException e){
            System.debug('Callout Exception on GET TOP LEVEL' + e);
        }

        // change source for next call
        source = 'MSI-Apex-buildSubTeams';

        // for each role that was added to the roleMap in the first call
        for(CustomRole currentRole : roleMap.values()){
           // set a dynamic endpoint to get the subteam based on currentRole.guid
           httpReq.setEndpoint(endpoint+'/teams/'+ currentRole.guid +'/teams?apikey='+apikey+'&source='+source);

            try{
                // Get HTTP Callout Response
                HTTPResponse httpResp = http.send(httpReq);

                // Create Parser
                JSONParser parser = JSON.createParser(httpResp.getBody());
                while(parser.nextToken() != null){
                    // if Current Token is the Id field, go to Next Token and set teamId
                    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
                    (parser.getText() == 'Id')){
                            parser.nextToken();
                            teamId = parser.getText();
                    }
                    // if Current Token is the Name field, go to Next Token and set teamName
                    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && 
                    (parser.getText() == 'Name')){
                        parser.nextToken();
                        teamName = parser.getText();       
                    }
                    // once we reach the end of the object...
                    if((parser.getCurrentToken() == JSONToken.END_OBJECT)){
                        // If the topLevelToIdMap contains the key (Internal or External)
                        if(roleMap.containsKey(currentRole.guid)){
                            roleMap.get(currentRole.guid).childRoles.put(teamId,new CustomRole(teamId,currentRole.guid,teamName));  
                        }

                        // then reset the team Id and team Name to null before next object in the parser
                        teamId = null;
                        teamName = null;   
                    }
                }
            }catch(System.CalloutException e){
                System.debug('Callout Exception on GET SUBTEAM LEVEL' + e);
            }  
        }
        System.debug('Final SubTeam Level KeySet: ' + roleMap.keySet());
        System.debug('Final SubTeam Level Values: ' + roleMap.values());      
    }   
}
  • As for the limits, I've never hit or seen a limit in relation to a map. Although when I'm using maps, if it gets to point where I have to go beyond one or two levels deep, I usually opt to use Wrapper Classes as it's usually more maintainable/readable at that point. – Antonio Manente Feb 21 '17 at 21:59
  • 3
    @AntonioManente The limit is 7. You're absolutely right, this sort of structure is incredibly hard to even read, much less utilize, but there is a hard limit to the number of nested classes you can have. – sfdcfox Feb 21 '17 at 23:27
10

There is a nested limit of 7 collections. If you attempt to go beyond this, you'll get an error:

Nested type exceeds maximum level: 8


First question: Is this even possible? Am I exceeding any limits with a Map like this? I have looked over documentation and can't find anything regarding nested map limits.

I know it's in the documentation[citation needed]†, but it's easy enough to test by using the following Execute Anonymous script:

Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Id>>>>>>>>> x;

However, assuming each role Id is unique, I don't see why you couldn't just use a simple:

Map<String, CustomRole> roles = new Map<String, CustomRole>();

Where CustomRole is just a class that stores the necessary data. To actually build the trees, perhaps in a Visualforce page, you still only need something like:

CustomRole[] roleTree = new CustomRole[0];

Where CustomRole would contain a list of children you could recursively iterate over. It'd probably be a lot easier to build the required functionality in JavaScript if that's an option.


Second question: Since I will be storing data incrementally in the map (as detailed above), I need to store data in the map as I get it, and continue to build the map as I go through the loops and make the calls. I've never dealt with a nested map before, especially not a 6-level-deep nested map. How would I properly insert data into the map for the second level while maintaining the third, fourth, fifth, and sixth levels as null, and then move on to storing data in the third level while keeping the fourth, fifth, and sixth null, and so on, until the map is complete?

If you really wanted to torture yourself with this logic, you simply need to keep track of which layer you're in. I'm not going to write this out explicitly, because just off the top of my head, it looks like you're going to be looking at the 100+ lines of code ballpark. This really isn't ideal when a simple recursive loop should be just fine. Starting with a simple Map<String, CustomRole> would be a lot easier to read and maintain.

† The documentation states there's actually a limit of four nested elements, for a total of five deep, but the prior limit is seven nested elements for a total of eight deep. Execute Anonymous currently honors the 8-level construct, but it seems that salesforce is planning on reducing it, either because nobody uses it, or for some other optimization.

  • Thanks! My exact need is, using data from contacts, I need to determine the group a contact should be assigned to in the exterior system, and then API call out to their service to register that user to that team in their system. This is ideally done in the background with no UI interaction outside of editing the contact record. I know this isn't a do my work for me forum but could you elaborate on your thoughts regarding the wrapper class? Would you do an Id, Name, and then List of children? Or just treat it like a record in an object and do "parent/child" with fields like id,parentid,name? – Morgan Marchese Feb 22 '17 at 14:39
  • To clarify for context - a Contact in SF will have "Language: English", "Type: Platform", "Location: Single", "Title: Manager". In the external system it is External > English > Platform > Single > Manager. I need to assign them to -that- Manager role, which is at the bottom of this confusing hierarchy schema. What data would be stored in CustomRole[] for me to be able to assign this specific contact to that specific Manager group among a list of 10 other Manager groups? – Morgan Marchese Feb 22 '17 at 16:37
  • I think I see where you're going with this, but you're using roleMap.get(currentRole.ParentId) portion loses me. I don't know the parentId during first query, so not sure how I can populate my original roleMap. Unfortunately this data doesn't exist in their JSON response. The only way for me to know ParentId is to first know the ID of all of the roles, then do a separate API call to get the children of each role. – Morgan Marchese Feb 22 '17 at 17:47
  • I understand that you are saying I need to end up with a map where Key = ID and Value = List of Child Roles, I just still am having trouble seeing how I am going to get there. I guess I've bitten off more than I can chew here, this is really pushing the boundaries of my level of knowledge. I'm about ready to just manually create the hierarchy in a custom object in Salesforce and chalk this up as a loss. Time is unfortunately not my friend on this project – Morgan Marchese Feb 22 '17 at 17:49
  • fox (can't tag you for some reason) - I think I've gotten a pretty good handle on what you were suggesting over the past few hours. I've written some code and updated my question to include it. I'm at the point now where I am trying to do the roleMap.get(currentRole.ParentID).put(currentRole.guid, new CustomRole(...)) logic and getting incorrect method or signature. I feel much closer now, and have the debug log showing TEAMS/SubTeams and Ids, but can't figure out how to populate the map within the CustomRole class. Can you assist? – Morgan Marchese Feb 23 '17 at 2:39
1

It doesn't look for me like you need this complex nested Map structure if you reverse the logic - having Map where key would be Id of element, and value parent of that element (or null for the element on top). I really don't understand why their JSON can't returning structure like

{
    "Id": "-7tu-ZPY3Ik1",
    "Name": "Spanish",
    "TeamCodeForBulkImport": "446885-Spanish",
    "ParentId": "pAFwnPpqJTo1"
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.