Лучший способ получить данные дерева в NHibernate - PullRequest
5 голосов
/ 12 апреля 2011

Я хочу получить иерархические / древовидные данные, как показано ниже, из таблицы, которая имеет следующее определение.

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

Обязательные данные результата запроса (нет необходимости вкладки) - Если я выбираю 'Work1', ядолжен завершить идентификаторы, которые находятся под его корнем что-то вроде ниже.Если я выберу «Work2», то я также должен заполнить идентификаторы выше и ниже его корня.

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

Каков наилучший способ в NHibernate для извлечения данных в вышеописанном сценарии оптимизированным способом.

1 Ответ

9 голосов
/ 13 апреля 2011

Чтобы выяснить, что такое «лучший способ», потребуется больше информации о реальном сценарии. Какую «оптимизацию» вы ищете? Минимальный объем данных (только те строки, которые вам действительно понадобятся) или минимальное количество 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);
    }
}

Edit:

Сценарий 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 - количество уровней (максимальная глубина дерева).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...