Иерархические данные в Linq - варианты и производительность - PullRequest
12 голосов
/ 15 октября 2008

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

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

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

Ответы [ 9 ]

15 голосов
/ 10 июля 2009

Эта опция также может оказаться полезной:

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

8 голосов
/ 23 марта 2010

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

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

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

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

3 голосов
/ 15 октября 2008

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

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

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

1 голос
/ 15 октября 2008

Я получил этот подход из блога Роба Конери (посмотрите код 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 голосов
/ 31 марта 2010

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

http://support.microsoft.com/default.aspx?scid=kb;en-us;q248915

0 голосов
/ 15 октября 2008

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

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

...