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

StackOverflow https://stackoverflow.com/questions/202912

Вопрос

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

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

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

Это было полезно?

Решение

Я бы настроил представление и связанную с ним табличную функцию на основе CTE.Мои доводы в пользу этого заключаются в том, что, хотя вы могли бы реализовать логику на стороне приложения, это потребовало бы отправки промежуточных данных по проводам для вычислений в приложении.Используя DBML designer, представление преобразуется в объект таблицы.Затем вы можете связать функцию с объектом Table и вызвать метод, созданный в 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...
    }
}

Другие советы

Это вариант это также может оказаться полезным:

Метод расширения LINQ AsHierarchy()
http://www.scip.be/index.php?Page=ArticlesNET18

Я удивлен, что никто не упомянул альтернативный дизайн базы данных - когда иерархию необходимо сгладить с нескольких уровней и извлекать с высокой производительностью (не учитывая пространство для хранения), лучше использовать другую таблицу entity-2-entity для отслеживания иерархии вместо подхода 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)
);

Я сделал это двумя способами:

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

Этот метод расширения потенциально может быть изменен для использования 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)

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

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

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

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
                                        }
                                      )
                             };

Проблема с получением данных со стороны клиента заключается в том, что вы никогда не можете быть уверены, насколько глубоко вам нужно зайти.Этот метод будет выполнять один круговой переход на глубину, и его можно было бы объединить, чтобы выполнить от 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;
}

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

Пожалуйста, прочтите следующую ссылку.

http://support.microsoft.com/default.aspx?scid=kb ;ru-США;q248915

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top