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