开发者

Build Tree more efficiently?

I was wondering if this code is good enough or if there are glaring newbie no-no's.

Basically I'm populating a TreeView listing all Departments in my database. Here is the Entity Framework model:

Build Tree more efficiently?

Here is the code in question:

private void button1_Click(object sender, EventArgs e)
{            
    DepartmentRepository repo = new DepartmentRepository();
    var parentDepartments = repo.FindAllDepartments()
                          .Where(d => d.IDParentDepartment == null)
                          .ToList();
    foreach (var parent in parentDepartments)
    {           
        TreeNode node = new TreeNode(parent.Name); 
        treeView1.Nodes.Add(node);

        var children = repo.FindAllDepartments()
                     .Where(x => x.IDParentDepartment == parent.ID)
                     .ToList();
        foreach (var child in children)
        {
            node.Nodes.Add(child.Name);                       
        }                
    }
}

EDIT:

Good suggestions so far. Working with the entire collect开发者_如何转开发ion makes sense I guess. But what happens if the collection is huge as in 200,000 entries? Wouldn't this break my software?

DepartmentRepository repo = new DepartmentRepository();
var entries = repo.FindAllDepartments();

var parentDepartments = entries
                      .Where(d => d.IDParentDepartment == null)
                      .ToList();
foreach (var parent in parentDepartments)
{
    TreeNode node = new TreeNode(parent.Name);
    treeView1.Nodes.Add(node);

    var children = entries.Where(x => x.IDParentDepartment == parent.ID)
                          .ToList();
    foreach (var child in children)
    {
        node.Nodes.Add(child.Name);
    }
}


Since you are getting all of the departments anyway, why don't you do it in one query where you get all of the departments and then execute queries against the in-memory collection instead of the database. That would be much more efficient.

In a more general sense, any database model that is recursive can lead to issues, especially if this could end up being a fairly deep structure. One possible thing to consider would be for each department to store all of its ancestors so that you would be able to get them all at once instead of having to query for them all at once.


In light of your edit, you might want to consider an alternative database schema that scales to handle very large tree structures.

There's a explanation on the fogbugz blog on how they handle hierarchies. They also link to this article by Joe Celko for more information.

Turns out there's a pretty cool solution for this problem explained by Joe Celko. Instead of attempting to maintain a bunch of parent/child relationships all over your database -- which would necessitate recursive SQL queries to find all the descendents of a node -- we mark each case with a "left" and "right" value calculated by traversing the tree depth-first and counting as we go. A node's "left" value is set whenever it is first seen during traversal, and the "right" value is set when walking back up the tree away from the node. A picture probably makes more sense:

The Nested Set SQL model lets us add case hierarchies without sacrificing performance.

How does this help? Now we just ask for all the cases with a "left" value between 2 and 9 to find all of the descendents of B in one fast, indexed query. Ancestors of G are found by asking for nodes with "left" less than 6 (G's own "left") and "right" greater than 6. Works in all databases. Greatly increases performance -- particularly when querying large hierarchies.

Build Tree more efficiently?


Assuming that you are getting the data from a database the first thing that comes to mind is that you are going to be hitting the database n+1 times for as many parents that you have in the database. You should try and get the whole tree structure out in one hit.

Secondly, you seem to get the idea patterns seeing as you appear to be using the repository pattern so you might want to look at IoC. It allows you to inject your dependency on a particular object such as your repository into your class where it is going to be used allowing for easier unit testing.

Thirdly, regardless of where you get your data from, move the structuring of the data into a tree data structure into a service which returns you an object containing all your departments that have already been organised (This basically becomes a DTO). This will help you reduce code duplication.

With anything you need to apply the yagni principle. This basically says that you should only do something if you are going to need it so if the code you have provided above is complete, needs no further work and is functional don't touch it. The same goes with the performance issue of select n+1, if you are not seeing any performance hits don't do anything as it may be premature optimization.


In your edit

DepartmentRepository repo = new DepartmentRepository();
var entries = repo.FindAllDepartments();

var parentDepartments = entries.Where(d => d.IDParentDepartment == null).ToList();
foreach (var parent in parentDepartments)
{
    TreeNode node = new TreeNode(parent.Name);
    treeView1.Nodes.Add(node);

    var children = entries.Where(x => x.IDParentDepartment == parent.ID).ToList();
    foreach (var child in children)
    {
        node.Nodes.Add(child.Name);
    }
}

You still have a n+1 issue. This is because the data is only retrieved from the database when you call the ToList() or when you iterate over the enumeration. This would be better.

var entries = repo.FindAllDepartments().ToList();

var parentDepartments = entries.Where(d => d.IDParentDepartment == null);
foreach (var parent in parentDepartments)
{
    TreeNode node = new TreeNode(parent.Name);
    treeView1.Nodes.Add(node);

    var children = entries.Where(x => x.IDParentDepartment == parent.ID);
    foreach (var child in children)
    {
        node.Nodes.Add(child.Name);
    }
}


That looks ok to me, but think about a collection of hundreds of thousands nodes. The best way to do that is asynchronous loading - please notice, that do don't necassarily have to load all elements at the same time. Your tree view can be collapsed by default and you can load additional levels as the user expands tree's nodes. Let's consider such case: you have a root node containing 100 nodes and each of these nodes contains at least 1000 nodes. 100 * 1000 = 100000 nodes to load - pretty much, istn't it? To reduce the database traffic you can first load your first 100 nodes and then, when user expands one of those, you can load its 1000 nodes. That will save considerable amount of time.


Things that come to mind:

It looks like .ToList() is needless. If you are simply iterating over the returned result, why bother with the extra step?

Move this function into its own thing and out of the event handler.

As other have said, you could get the whole result in one call. Sort by IDParentDepartment so that the null ones are first. That way, you should be able to get the list of departments in one call and iterate over it only once adding children departments to already existing parent ones.


Wrap the TreeView modifications with:

treeView.BeginUpdate(); // modify the tree here. treeView.EndUpdate();

To get better performance.

Pointed out here by jgauffin


This should use only one (albeit possibly large) call to the database:

Departments.Join(
        Departments,
        x => x.IDParentDepartment,
        x => x.Name,
        (o,i) => new { Child = o, Parent = i }
    ).GroupBy(x => x.Parent)
    .Map(x => {
            var node = new TreeNode(x.Key.Name);
            x.Map(y => node.Nodes.Add(y.Child.Name));
            treeView1.Nodes.Add(node);
        }
    )

Where 'Map' is just a 'ForEach' for IEnumerables:

public static void Map<T>(this IEnumerable<T> source, Action<T> func)
{
    foreach (T i in source)
        func(i);
}

Note: This will still not help if the Departments table is huge as 'Map' materializes the result of the sql statement much like 'ToList()' does. You might consider Piotr's answer.


In addition to Bronumski and Keith Rousseau answer Also add the DepartmentID with the nodes(Tag) so that you don't have to re-query the database to get the departmentID

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜