Иерархические данные в Linq - параметры и производительность

У меня есть некоторые иерархические данные - каждая запись имеет идентификатор родительской записи id и (nullable). Я хочу получить все записи в дереве под данной записью. Это находится в базе данных SQL Server 2005. Я запрашиваю его с LINQ to SQL в С# 3.5.

LINQ to SQL не поддерживает Общие выражения таблицы напрямую. Мой выбор состоит в том, чтобы собрать данные в коде с несколькими запросами LINQ или сделать представление о базе данных, которая покрывает CTE.

Какой вариант (или другой вариант), по вашему мнению, будет лучше работать при больших объемах данных? Является ли SQL Server 2008 HierarchyId type поддерживается в Linq to SQL?

Ответ 1

Я бы создал представление и связанную с таблицей функцию на основе CTE. Мое рассуждение заключается в том, что, хотя вы можете реализовать логику на стороне приложения, это будет связано с отправкой промежуточных данных по проводу для вычисления в приложении. Используя конструктор DBML, представление преобразуется в объект таблицы. Затем вы можете связать функцию с объектом таблицы и вызвать метод, созданный в DataContext, для получения объектов типа, определенного представлением. Использование функции, основанной на таблицах, позволяет механизму запроса учитывать ваши параметры при построении набора результатов, а не применять условие к набору результатов, определяемому представлением после факта.

CREATE TABLE [dbo].[hierarchical_table](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [parent_id] [int] NULL,
    [data] [varchar](255) NOT NULL,
 CONSTRAINT [PK_hierarchical_table] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE VIEW [dbo].[vw_recursive_view]
AS
WITH hierarchy_cte(id, parent_id, data, lvl) AS
(SELECT     id, parent_id, data, 0 AS lvl
      FROM         dbo.hierarchical_table
      WHERE     (parent_id IS NULL)
      UNION ALL
      SELECT     t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl
      FROM         dbo.hierarchical_table AS t1 INNER JOIN
                            hierarchy_cte AS h ON t1.parent_id = h.id)
SELECT     id, parent_id, data, lvl
FROM         hierarchy_cte AS result


CREATE FUNCTION [dbo].[fn_tree_for_parent] 
(
    @parent int
)
RETURNS 
@result TABLE 
(
    id int not null,
    parent_id int,
    data varchar(255) not null,
    lvl int not null
)
AS
BEGIN
    WITH hierarchy_cte(id, parent_id, data, lvl) AS
   (SELECT     id, parent_id, data, 0 AS lvl
        FROM         dbo.hierarchical_table
        WHERE     (id = @parent OR (parent_id IS NULL AND @parent IS NULL))
        UNION ALL
        SELECT     t1.id, t1.parent_id, t1.data, h.lvl + 1 AS lvl
        FROM         dbo.hierarchical_table AS t1 INNER JOIN
            hierarchy_cte AS h ON t1.parent_id = h.id)
    INSERT INTO @result
    SELECT     id, parent_id, data, lvl
    FROM         hierarchy_cte AS result
RETURN 
END

ALTER TABLE [dbo].[hierarchical_table]  WITH CHECK ADD  CONSTRAINT [FK_hierarchical_table_hierarchical_table] FOREIGN KEY([parent_id])
REFERENCES [dbo].[hierarchical_table] ([id])

ALTER TABLE [dbo].[hierarchical_table] CHECK CONSTRAINT [FK_hierarchical_table_hierarchical_table]

Чтобы использовать его, вы бы сделали что-то вроде: при условии разумной схемы именования:

using (DataContext dc = new HierarchicalDataContext())
{
    HierarchicalTableEntity h = (from e in dc.HierarchicalTableEntities
                                 select e).First();
    var query = dc.FnTreeForParent( h.ID );
    foreach (HierarchicalTableViewEntity entity in query) {
        ...process the tree node...
    }
}

Ответ 3

Я удивлен, что никто не упомянул альтернативный дизайн базы данных - когда иерархию нужно сгладить с нескольких уровней и получить с высокой производительностью (не учитывая пространство для хранения), лучше использовать другую таблицу сущность-2-сущность для отслеживания иерархии вместо подхода parent_id.

Это позволит не только отношениям с одним родителем, но и отношениям с несколькими родителями, указателями уровня и различными типами отношений:

CREATE TABLE Person (
  Id INTEGER,
  Name TEXT
);

CREATE TABLE PersonInPerson (
  PersonId INTEGER NOT NULL,
  InPersonId INTEGER NOT NULL,
  Level INTEGER,
  RelationKind VARCHAR(1)
);

Ответ 4

Я сделал два способа:

  • Воспроизводить каждый слой дерева на основе пользовательского ввода. Представьте, что элемент управления древовидной структурой заполнен корнем node, дочерними элементами корня и внуками корня. Расширяются только корень и дети (внуки скрыты с крахом). По мере того, как пользователь расширяет дочерний элемент node, внуки корня отображаются (которые ранее были извлечены и скрыты), и начинается поиск всех правнуков. Повторите шаблон для глубоких слоев N. Этот шаблон работает очень хорошо для больших деревьев (глубина или ширина), потому что он только извлекает часть необходимого дерева.
  • Используйте хранимую процедуру с LINQ. Используйте что-то вроде обычного табличного выражения на сервере для построения результатов в плоской таблице или построения дерева XML в T-SQL. У Скотта Гатри отличная статья об использовании хранимых procs в LINQ. Создайте свое дерево из результатов, когда они вернутся, если в плоском формате, или используйте дерево XML, если это то, что вы возвращаете.

Ответ 5

Этот метод расширения потенциально может быть изменен для использования IQueryable. Я использовал его в прошлом в коллекции объектов. Он может работать для вашего сценария.

public static IEnumerable<T> ByHierarchy<T>(
 this IEnumerable<T> source, Func<T, bool> startWith, Func<T, T, bool> connectBy)
{
  if (source == null)
   throw new ArgumentNullException("source");

  if (startWith == null)
   throw new ArgumentNullException("startWith");

  if (connectBy == null)
   throw new ArgumentNullException("connectBy");

  foreach (T root in source.Where(startWith))
  {
   yield return root;
   foreach (T child in source.ByHierarchy(c => connectBy(root, c), connectBy))
   {
    yield return child;
   }
 }
}

Вот как я его назвал:

comments.ByHierarchy(comment => comment.ParentNum == parentNum, 
 (parent, child) => child.ParentNum == parent.CommentNum && includeChildren)

Этот код является улучшенной, исправленной ошибкой версией кода, найденного здесь.

Ответ 6

В MS SQL 2008 вы можете напрямую использовать HierarchyID, в sql2005 вам, возможно, придется реализовать их вручную. ParentID не работает на больших наборах данных. Также проверьте эту статью для более подробного обсуждения темы.

Ответ 7

Я получил этот подход от Блог Роба Конира (отметьте около 6 для этого кода, также на codeplex), и я люблю его использовать, Это может быть изменено для поддержки нескольких уровней "sub".

var categories = from c in db.Categories
                 select new Category
                 {
                     CategoryID = c.CategoryID,
                     ParentCategoryID = c.ParentCategoryID,
                     SubCategories = new List<Category>(
                                      from sc in db.Categories
                                      where sc.ParentCategoryID == c.CategoryID
                                      select new Category {
                                        CategoryID = sc.CategoryID, 
                                        ParentProductID = sc.ParentProductID
                                        }
                                      )
                             };

Ответ 8

Проблема с извлечением данных с клиентской стороны заключается в том, что вы никогда не можете быть уверены в том, насколько глубоко вам нужно идти. Этот метод будет делать одно округление на глубину, и его можно было бы объединить с 0 до заданной глубины в одном обратном направлении.

public IQueryable<Node> GetChildrenAtDepth(int NodeID, int depth)
{
  IQueryable<Node> query = db.Nodes.Where(n => n.NodeID == NodeID);
  for(int i = 0; i < depth; i++)
    query = query.SelectMany(n => n.Children);
       //use this if the Children association has not been defined
    //query = query.SelectMany(n => db.Nodes.Where(c => c.ParentID == n.NodeID));
  return query;
}

Однако он не может выполнять произвольную глубину. Если вам действительно нужна произвольная глубина, вам нужно сделать это в базе данных, чтобы вы могли принять правильное решение для остановки.