在NHibernate中获取树数据的最佳方法

时间:2011-04-12 16:20:45

标签: nhibernate

我想从具有以下定义的表中获取下面的Hierarchical / Tree数据。

Tree Table:
"""""""""""
Id   |ParentId
"""""""""""
Work1|null
Work2|Work1
Work3|Work2
...

必需的查询结果数据(不需要选项卡) - 如果我选择'Work1',我应该完成其根目录下的ID。如果我选择'Work2',那么我也应该在其根之上和之下完成ID。

> Work1 
----------
>   Work2
----------
>     Work3
---------

NHibernate以优化的方式在上述场景中获取数据的最佳方式是什么。

1 个答案:

答案 0 :(得分:9)

要了解“最佳方式”是什么,需要有关实际方案的更多信息。您在寻找什么样的“优化”?最少量的数据(只有你真正需要的行)或最少数量的SQL查询(最好是一次到数据库的往返)或其他任何数据?

场景1:一次加载并在内存中保留更长时间的菜单或树结构(不是每隔几秒更新一次的列表)。表中的行数很少(小是相对的,但我会说200以下的任何行)。

在这种情况下,我只需要使用一个查询来获取整个表:

var items = session.Query<Work>()
    .Fetch(c => c.ParentWork)
    .Fetch(c => c.ChildWorks).ToList();

var item = session.Get<Work>(id);

这将导致单个SQL查询,它只是从表中加载所有行。 item将包含完整的树(父母,祖父母,孩子等)。

场景2:大量行,只需要一小部分行。预计层次结构中只有少数级别。

在这种情况下,只需加载项目并通过延迟加载让NHibernate到其余部分或强制它通过编写递归方法来遍历父项和子项来加载所有内容。这将导致N + 1选择,这可能会或可能不会慢于方案1(取决于您的数据)。

这是一个快速的黑客证明:

var item = session.Get<Work>(id);

Work parent = item.ParentWork;
Work root = item;
// find the root item
while (parent != null)
{
    root = parent;
    parent = parent.ParentWork;
}
// scan the whole tree
this.ScanChildren(root);
// -----
private void ScanChildren(Work item)
{
    if (item == null)
    {
        return;
    }

    foreach (Work child in item.ChildWorks)
    {
        string name = child.Name;
        this.ScanChildren(child);
    }
}

修改

场景3:大量数据。最少的查询次数和最少的数据量。

在这种情况下,我认为不是树结构,而是我们一个接一个地加载数据层。

var work = repo.Session.Get<Work>(id);

// get root of that Work
Work parent = work.ParentWork;
Work root = work;
while (parent != null)
{
    root = parent;
    parent = parent.ParentWork;
}

// Get all the Works for each level
IList<Work> worksAll = new List<Work>() { root };
IList<Work> worksPerLevel = new List<Work>() { root };

// get each level until we don't have any more Works in the next level
int count = worksPerLevel.Count;
while (count > 0)
{
    worksPerLevel = this.GetChildren(session, worksPerLevel);
    // add the Works to our list of all Works
    worksPerLevel.ForEach(c => worksAll.Add(c));
    count = worksPerLevel.Count;
}

// here you can get the names of the Works or whatever
foreach (Work c in worksAll)
{
    string s = c.Name;
}

// this methods gets the Works in the next level and returns them
private IList<Work> GetChildren(ISession session, IList<Work> worksPerLevel)
{
    IList<Work> result = new List<Work>();
    // get the IDs for the works in this level
    IList<int> ids = worksPerLevel.Select(c => c.Id).ToList();

    // use a WHERE IN clause do get the Works 
    // with the ParentId of Works in the current level
    result = session.QueryOver<Work>()
        .Where(
            NHibernate.Criterion.Restrictions.InG<int>(
                NHibernate.Criterion.Projections.Property<Work>(
                    c => c.ParentWork.Id),
                ids)
        )
        .Fetch(c => c.ChildWorks).Eager // this will prevent the N+1 problem
        .List();

    return result;
}

这个解决方案不会导致N + 1问题,因为我们对子节点使用了一个急切的负载,所以NHibernate会知道子列表的状态而不是再次命中数据库。您将只获得x + y选择,其中x是查找根Work的选择数,y是级别数(树的最大深度)。