Take the 2-minute tour ×
Salesforce Stack Exchange is a question and answer site for Salesforce administrators, implementation experts, developers and anybody in-between. It's 100% free, no registration required.

In a managed package, I have an object Project_Task__c that can be related to itself (its parent), N-levels deep. I'd like to be able to extract all my tasks and serialize them into JSON as:

[
    { 
        "TaskId" : "1",
        "TaskName" : "Parent 1",
        "children" : [
            { 
                "TaskId" : "2",
                "TaskName" : "Child 1.1",
                "children" : []
            },
            { 
                "TaskId" : "3",
                "TaskName" : "Child 1.2",
                "children" : [
                    { 
                        "TaskId" : "4",
                        "TaskName" : "Child 1.2.1",
                        "children" : []
                    }
                ]
            }
        ]
    },
    {
        "TaskId" : "5",
        "TaskName" : "Parent 2",
        "children" : []
    }
]

I've created a SOQL statement to get my Tasks, and a wrapper class to wrap around each task:

public class TaskHierarchyWrapper {

    public String TaskId {get; set;}
    public String TaskName {get; set;}
    public List<TaskHierarchyWrapper> children {get; set;}

    public TaskHierarchyWrapper()
    {
        this.children = new List<TaskHierarchyWrapper>();
    }

} 

And I can obviously serialize this object using JSON.serialize(), but is there a way to assemble this hierarchy of objects without writing a big, ugly FOR loop?

share|improve this question
1  
So the actual issue is not with the JSON serializing? You just want to know the best way to take a list of project_tasks and build the TaskHierarchyWrapper tree from them? –  Greg Grinberg Jan 15 '13 at 22:54
    
Thats correct. The for loop even is not the biggest concern, its having a recursive method to traverse the tree each time to determine where to insert the next node. –  sheanineseven Jan 16 '13 at 1:37
add comment

3 Answers

There is no SOQL where you can get the top level, its children, and its children's children, etc., because you can't go more than one level deep on the related lists. I think you need to have a SOQL query where you get all of the tasks in which you are interested and their parent and grandparent.

To me, the somewhat less obvious part would be coming up with a solution that avoids using a nested loop. You don't want to go back over the List with every new task processed, looking for its parent to get it's children list to add the task to. If you keep a Map of task Id to TaskHierarchyWrapper, in addition to the list of top level TaskHierarchyWrappers, then you can avoid nested loops.

Here's code that builds a list of TaskHierarchyWrapper that can then be passed to the JSON.serialize method. The basic idea behind the code is pretty simple.

  1. Process the task.
  2. Process the parent of the task.
  3. Add the task to the children list of the parent.

The key is making sure to query for the grand parent (i.e., parent's parent), so that when you are processing a parent you know whether it is a top level (i.e., parent-less) task.

It is possible that a parent task will be added to the hierarchy before it is encountered on the allTasks list (i.e., before it is the main task in the loop). At that point it will not have its parent's fields available, so it won't be added to its parent's list of children. Later, when it is the current task being processed it will already be on the hierarchy, so it won't get added; however, it will add itself to its parent's list of children, because at that point it will have its parent's fields available.

The loop will be executed N times. There's some room in it to reduce script statements if that's needed, but I didn't do that in favor of readability.

Note: I just used the record Id for Task Id and Name for Task Name

public class TaskHierarchyWrapper {

    public String TaskId {get; set;}
    public String TaskName {get; set;}
    public List<TaskHierarchyWrapper> children {get; set;}

    public TaskHierarchyWrapper(Project_Task__c task) {
        taskId = task.Id;
        taskName = task.Name;
        this.children = new List<TaskHierarchyWrapper>();
    }

    public static List<TaskHierarchyWrapper> buildWrappers(List<Project_Task__c> allTasks) {
        Map<Id, TaskHierarchyWrapper> taskMap = new Map<Id, TaskHierarchyWrapper>();
        List<TaskHierarchyWrapper> topLevelTasks = new List<TaskHierarchyWrapper>();

        for (Project_Task__c task : allTasks) {
            TaskHierarchyWrapper taskWrapper = processTask(task, taskMap, topLevelTasks);
            if (task.Parent_Task__c != null) {
                TaskHierarchyWrapper parentTaskWrapper = processTask(task.Parent_Task__r, taskMap, topLevelTasks);
                parentTaskWrapper.children.add(taskWrapper);
            }
        }

        return topLevelTasks;
    }

    private static TaskHierarchyWrapper processTask(
        Project_Task__c task, 
        Map<Id,TaskHierarchyWrapper> taskMap,
        List<TaskHierarchyWrapper> topLevelTasks
    ) {
        // The wrapper may already exist if it was processed as the parent
        // of another task
        TaskHierarchyWrapper wrapper = taskMap.get(task.Id);

        if (wrapper == null) {
            wrapper = new TaskHierarchyWrapper(task);
            taskMap.put(task.Id, wrapper);

            // Add to the top level list if there's no parent
            // This works for the parent as well because the grandparent
            // is included in the original SOQL query
            if (task.Parent_Task__c == null) {
                topLevelTasks.add(wrapper);
            }
        }
        return wrapper; 
    }

    @isTest
    static void testBuildingWrapper() {
        setupTasks();  // somehow insert them.

        // Get all of the tasks along with their parent and grandparent
        List<Project_Task__c> allTasks = [
            Select Id, Name,
                   Parent_Task__c, Parent_Task__r.Id, Parent_Task__r.Name,
                   Parent_Task__r.Parent_Task__c
            From Project_Task__c
        ];

        Test.startTest();

        List<TaskHierarchyWrapper> wrappers = buildWrappers(allTasks);

        Test.stopTest();

    }
}
share|improve this answer
add comment

Your question may well be in vain here. If you find a way which doesn't involve a loop then you'll simply be leveraging some pre-built functionality which will inevitably include a loop regardless.

So if your question is one of aesthetics then there may be a way to avoid writing the loop yourself, but if it's one of performance with the perception that a loop will be slow, chances are it's moot. Many operations (clearing an array to zero for instance) come down to a very basic loop at the end of the day and exotic hardware resets aside, there's only one real implementation when you get down to the nuts and bolts.

share|improve this answer
3  
True, although one of the major concerns I'd have in this situation is script statement and heap use. Any ways that you can defer looping to the underlying System apex methods are a significant gain in scalability. –  ca_peterson Jan 16 '13 at 0:22
    
That's a good point, it definitely comes down to context & data volumes etc. –  LaceySnr Jan 16 '13 at 0:51
add comment

As it turns out, when "N" is relatively small (ex. 5 or less) and there are a relatively small number of total nodes (ex. 50), the performance of using recursion is actually not bad at all (ex. 1-3 second response time).

Increasing "N", or the number of total nodes will obviously degrade performance, the question is: how quickly will it degrade (i'll post my findings).

Also, using Peter's suggestion of using a Map to avoid nested loops would likely help a good deal.

This was my first draft, using recursion:

public class TaskHierarchyParser {

    public static void InsertNode(List<TaskHierarchy> currentHierarchy, TaskHierarchy newHierarchyNode)
    {
        if(newHierarchyNode.parentId == null)
        {
            currentHierarchy.add(newHierarchyNode);
        }
        else
        {
            for (TaskHierarchy currentNode : currentHierarchy)
            {                  
                if(newHierarchyNode.parentId == currentNode.Id)
                {
                    currentNode.children.add(newHierarchyNode);
                    break;
                }
                else
                {
                    TaskHierarchyParser.InsertNode(currentNode.children, newHierarchyNode);
                }
            }
        }
    }
} 
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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