2008-10-14 10 views
11

Mam pewne dane hierarchiczne - każdy wpis ma id i (Nullable) nadrzędny wpis id. Chcę pobrać wszystkie wpisy w drzewie pod danym wpisem. Jest to w bazie danych SQL Server 2005. Pytam o to za pomocą LINQ do SQL w C# 3.5.Hierarchiczne dane w Linq - opcje i wydajność

LINQ do SQL nie obsługuje bezpośrednio Common Table Expressions. Moim wyborem jest złożenie danych w kodzie za pomocą kilku zapytań LINQ lub zrobienie widoku na bazie danych, która przedstawia CTE.

Którą opcję (lub inną opcję) uważasz, że będzie lepiej działać, gdy ilość danych się zwiększy? Czy program SQL Server 2008's HierarchyId type jest obsługiwany w Linq na SQL?

Odpowiedz

6

Chciałbym skonfigurować widok i powiązaną funkcję opartą na tabeli na podstawie CTE. Moje rozumowanie jest takie, że chociaż można wdrożyć logikę po stronie aplikacji, będzie to wymagało wysłania pośrednich danych przez przewód w celu obliczenia w aplikacji. Za pomocą projektanta DBML widok przekształca się w encję tabeli. Następnie można powiązać tę funkcję z jednostką Table i wywołać metodę utworzoną w DataContext, aby wyprowadzić obiekty typu zdefiniowanego przez widok. Korzystanie z funkcji opartej na tabelach umożliwia mechanizmowi zapytania uwzględnienie parametrów podczas konstruowania zestawu wyników, zamiast stosowania warunku w zestawie wyników zdefiniowanym w widoku po fakcie.

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] 

Aby go użyć by zrobić coś podobnego - zakładając jakiś rozsądny schemat nazewnictwa:

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... 
    } 
} 
+1

Próbowałem takiej funkcji i wydaje się, że jest to właściwa droga. I może być wywołany z LINQ, dołączony do datacontext. Dlaczego zarówno widok, jak i funkcja? - wydają się być duplikowane. – Anthony

+1

Funkcja nie mapuje tego samego schematu co tabela. Obejmuje poziom. Jeśli nie masz dodanej kolumny, możesz zmapować ją bezpośrednio na stół. Zakładałem, że poziom w hierarchii jest ważny. – tvanfosson

2

W MS SQL 2008 można użyć bezpośrednio HierarchyID, w sql2005 może być konieczne zaimplementowanie ich ręcznie. ParentID nie działa na dużych zbiorach danych. Sprawdź także this article, aby uzyskać więcej informacji na ten temat.

+0

Nie ma tam wzmianki jeśli hierarchyid jest użyteczny w LINQ to SQL – Anthony

+1

wow, jest to z pewnością odpowiedź. miły post! – Shawn

+0

Nie można go używać w Linq2sql po wyjęciu z pudełka. –

3

mam zrobić to na dwa sposoby:

  1. napędzają pobieranie każdej z warstw drzewo na podstawie danych wprowadzonych przez użytkownika. Wyobraź sobie kontrolkę widoku drzewa zapełnioną węzłem głównym, potomkami korzenia i wnukami korzenia. Rozbudowuje się tylko korzeń i dzieci (wnuki są ukryte wraz z upadkiem). Gdy użytkownik rozwija węzeł podrzędny, wyświetlane są wnuki korzenia (które wcześniej były pobierane i ukrywane), a następnie uruchamiane jest pobieranie wszystkich prawnuków. Powtórz wzór dla głębokich warstw N. Ten wzór działa bardzo dobrze w przypadku dużych drzew (głębokość lub szerokość), ponieważ pobiera tylko część potrzebnego drzewa.
  2. Użyj procedury przechowywanej z LINQ. Użyj czegoś podobnego do wspólnego wyrażenia tabelowego na serwerze, aby utworzyć wyniki w płaskiej tabeli lub zbuduj drzewo XML w języku T-SQL. Scott Guthrie ma great article o używaniu przechowywanych procedur w LINQ. Zbuduj drzewo z wyników, gdy wrócą, jeśli jest w formacie płaskim, lub użyj drzewa XML, jeśli to jest to, co zwracasz.
+1

Byłem pogrążony w żałobie w znalezieniu rozwiązania tego problemu, gdy twoja odpowiedź otworzyła mi umysł na fakt, że nie muszę ciągnąć całego drzewa, po prostu ciągnąć dzieci w razie potrzeby. – ProfK

3

Ta metoda rozszerzenia może potencjalnie zostać zmodyfikowana w celu użycia IQueryable. Używałem go z powodzeniem w przeszłości w kolekcji przedmiotów. Może działać w twoim scenariuszu.

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

Oto jak nazwał go:

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

Ten kod jest ulepszony, bug-ustalona wersja kodu znalezione here.

+0

Możesz też sprawdzić, skąd go wziął: http://weblogs.asp.net/okloeten/archive/2006/07/09/Hierarchical-Linq-Queries.aspx – TheSoftwareJedi

+1

Dodałem atrybucję do Jedi. Moja wersja jest uproszczona i ulepszona. – JarrettV

1

Mam to podejście od Rob Conery's blog (sprawdź wokół Pt 6 dla tego kodu, także na codeplex) i uwielbiam go używać. Można to przebudować, aby obsługiwać wiele poziomów "podrzędnych".

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

Ale czy można go przebudować, aby obsługiwał nieograniczoną liczbę podpoziomów? – Anthony

+0

Nie zamierzasz dodawać do tego zapytania kilkunastu podkategorii - nie jest to szczególnie elastyczne. –

0

Kłopot ze ściąganiem danych od strony klienta polega na tym, że nigdy nie można być pewnym, jak głęboko należy się udać. Ta metoda wykona jeden objazd na głębokość i może być wykonywana od 0 do określonej głębokości w jednym obiegu.

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

Nie może jednak wykonywać arbitralnej głębokości. Jeśli naprawdę potrzebujesz arbitralnej głębokości, musisz to zrobić w bazie danych - abyś mógł podjąć właściwą decyzję, aby przestać.

8

jestem nikt zaskoczony wspomniał alternatywny projekt bazy danych - gdy hierarchia musi być spłaszczone od wielu poziomach i pobierane z wysoką wydajnością (nie biorąc pod uwagę przestrzeni dyskowej) lepiej jest użyć innej tabeli encji-2-encji do śledzenia hierarchii zamiast podejścia parent_id.

To pozwoli nie tylko pojedyncze relacje macierzystym, lecz również wielo relacji rodzic, wskazania poziomu i różne rodzaje relacji:

CREATE TABLE Person (
    Id INTEGER, 
    Name TEXT 
); 

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

Nie podoba mi się ta metoda - pętle "while" nie są zbyt dobrą praktyką SQL, a jeśli istnieje bardziej deklaratywny sposób, powinno to być preferowane. I jest teraz: użyj widoku lub funkcji opartej na tabelach, używając wspólnego wyrażenia tabelowego, używając konstrukcji WITH .. UNION ALL, jak pokazano w innych odpowiedziach tutaj. – Anthony

+0

Należy rozważyć umieszczenie fragmentu rozwiązania na stronie, którą połączyłeś. Linki mogą być martwe pewnego dnia. – rcdmk