SQL Server EF Core Sağlayıcısında Hiyerarşik Veriler

Note

Bu özellik EF Core 8.0'a eklendi.

Azure SQL ve SQL Server, hiyerarşik verileri depolamak için kullanılan adlı hierarchyid özel bir veri türüne sahiptir. Bu durumda, "hiyerarşik veriler", temelde her öğenin bir üst ve/veya alt öğeye sahip olabileceği bir ağaç yapısı oluşturan veriler anlamına gelir. Bu tür verilere örnek olarak şunlar verilebilir:

  • Kuruluş yapısı
  • Dosya sistemi
  • Projedeki görev kümesi
  • Dil terimlerinin taksonomisi
  • Web sayfaları arasındaki bağlantıların grafiği

Veritabanı daha sonra hiyerarşik yapısını kullanarak bu verilere karşı sorgu çalıştırabilir. Örneğin, bir sorgu belirli öğelerin üst öğelerini ve bağımlılarını bulabilir veya hiyerarşide belirli bir derinlikteki tüm öğeleri bulabilir.

.NET ve EF Core'da HierarchyId Kullanma

En düşük düzeyde, Microsoft.SqlServer.Types NuGet paketi adlı SqlHierarchyIdbir tür içerir. Bu tür çalışma hiyerarşisi değerlerini desteklese de LINQ ile çalışmak biraz zahmetlidir.

Sonraki düzeyde, yeni Microsoft.EntityFrameworkCore.SqlServer.Abstractions paketi kullanıma sunulmuştur ve varlık türlerinde kullanılması amaçlanan daha üst düzey bir HierarchyId tür içermektedir.

Tip

Tür HierarchyId, .NET normlarına daha uygun olup, SqlHierarchyId ise .NET Framework türlerinin SQL Server veritabanı motorunda barındırılma şekline göre modellenmiştir. HierarchyId EF Core ile çalışacak şekilde tasarlanmıştır, ancak diğer uygulamalarda EF Core dışında da kullanılabilir. Paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions başka hiçbir pakete başvurmaz ve dağıtılan uygulama boyutu ve bağımlılıkları üzerinde en az etkiye sahiptir.

HierarchyId EF Core işlevleri, örneğin sorgular ve güncelleştirmeler için, Microsoft.EntityFrameworkCore.SqlServer.HierarchyId paketi gerektirir. Bu paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions ve Microsoft.SqlServer.Types öğelerini geçişli bağımlılıklar olarak getirir ve bu nedenle çoğunlukla gereken tek pakettir.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer.HierarchyId

Paket yüklendikten sonra, HierarchyId kullanımı, uygulamanın UseSqlServer çağrısına dahil edilerek UseHierarchyId çağrısıyla etkinleştirilmiş olur. Örneğin:

options.UseSqlServer(
    connectionString,
    x => x.UseHierarchyId());

Modelleme hiyerarşileri

Türü HierarchyId bir varlık türünün özellikleri için kullanılabilir. Örneğin, bazı kurgusal yarımlamaların babalık aile ağacını modellemek istediğimizi varsayalım. Halfling için varlık türünde, aile ağacındaki her yarı kanı bulmak için bir HierarchyId özelliği kullanılabilir.

public class Halfling
{
    public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null)
    {
        PathFromPatriarch = pathFromPatriarch;
        Name = name;
        YearOfBirth = yearOfBirth;
    }

    public int Id { get; private set; }
    public HierarchyId PathFromPatriarch { get; set; }
    public string Name { get; set; }
    public int? YearOfBirth { get; set; }
}

Tip

Aşağıda ve buradaki örneklerde gösterilen kod, HierarchyIdSample.cs'den gelmektedir.

Tip

İsterseniz, HierarchyId anahtar özellik türü olarak kullanmak için uygundur.

Bu durumda, aile ağacının kökü ailenin patriğiyle birlikte oluşturulur. Her yarımlama, kendi özelliği kullanılarak PathFromPatriarch ağaçta patrikten izlenebilir. SQL Server bu yollar için kompakt bir ikili biçim kullanır, ancak kodla çalışırken insan tarafından kolayca okunabilen bir dize gösterimine dönüştürmek ve buradan ayrıştırmak yaygın bir durumdur. Bu gösterimde, her düzeydeki konum bir / karakterle ayrılır. Örneğin, aşağıdaki diyagramda yer alan aile ağacını göz önünde bulundurun:

Yarım aile ağacı

Bu ağaçta:

  • Balbo, tarafından /temsil edilen ağacın kökündedir.
  • Balbo'nun , , /1//2//3/, ve /4/ile /5/temsil edilen beş çocuğu vardır.
  • Balbo'nun ilk çocuğu Mungo'nun da , , /1/1//1/2/, /1/3/ve /1/4/ile /1/5/temsil edilen beş çocuğu vardır. Mungo'nun (/1/) tüm çocukları için ön ek olarak kullanılan HierarchyId'ya dikkat edin.
  • Benzer şekilde Balbo'nun üçüncü çocuğu Ponto'nun ve /3/1/ile /3/2/ temsil edilen iki çocuğu vardır. Her bir çocuk, /3/ olarak temsil edilen Ponto için HierarchyId ön ekiyle başlar.
  • Ve bu şekilde ağaç aşağı doğru devam eder...

Aşağıdaki kod, EF Core kullanarak bu aile ağacını bir veritabanına ekler:

await AddRangeAsync(
    new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
    new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
    new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
    new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
    new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
    new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
    new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
    new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
    new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
    new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
    new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
    new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
    new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
    new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
    new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
    new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
    new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
    new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
    new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
    new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
    new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
    new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
    new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
    new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
    new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
    new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
    new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
    new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
    new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
    new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));

await SaveChangesAsync();

Tip

Gerekirse, ondalık değerler mevcut iki düğüm arasında yeni düğümler oluşturmak için kullanılabilir. Örneğin, /3/2.5/2/ ile /3/2/2/ arasında /3/3/2/ yer alır.

Hiyerarşileri sorgulama

HierarchyId LINQ sorgularında kullanılabilecek çeşitli yöntemleri kullanıma sunar.

Method Description
GetAncestor(int n) Hiyerarşik ağaçta n üst düzeydeki düğümü alır.
GetDescendant(HierarchyId? child1, HierarchyId? child2) Bir child1 değerinden büyük ve child2'den küçük olan bir alt düğümün değerini alır.
GetLevel() Hiyerarşik ağaçta bu düğümün seviyesini alır.
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) Yeni bir düğümün, newRoot konumundan oldRoot yoluna eşit bir yol izleyerek bu konuma gelmesine olanak tanıyan konumu temsil eden bir değer alır, bu da etkili bir şekilde bu konumu yeni konuma taşır.
IsDescendantOf(HierarchyId? parent) Bu düğümün alt parentöğesinin olup olmadığını belirten bir değer alır.

Ayrıca , , ==!=, <ve <=> işleçleri >=kullanılabilir.

Aşağıda, LINQ sorgularında bu yöntemleri kullanma örnekleri verilmiştir.

Ağaçta verilen bir seviyedeki varlıkları almak

Aşağıdaki sorgu, aile ağacında belirli bir düzeydeki tüm yarımları döndürmek için kullanır GetLevel :

var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0

Bunu bir döngüde çalıştırarak her nesil için halfling'leri alacağız:

Generation 0: Balbo
Generation 1: Mungo, Pansy, Ponto, Largo, Lily
Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
Generation 5: Angelica

Bir varlığın doğrudan atasını al

Aşağıdaki sorgu, verilen yarı cinin adı göz önünde bulundurularak bir yarı cini doğrudan atası bulmak için GetAncestor kullanır.

async Task<Halfling?> FindDirectAncestor(string name)
    => await context.Halflings
        .SingleOrDefaultAsync(
            ancestor => ancestor.PathFromPatriarch == context.Halflings
                .Single(descendent => descendent.Name == name).PathFromPatriarch
                .GetAncestor(1));

Bu, aşağıdaki SQL'e çevrilir:

SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch] = (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0).GetAncestor(1)

"Hobbit "Bilbo" için bu sorgu çalıştırıldığında "Bungo" döndürülüyor."

Bir varlığın doğrudan alt öğelerini alma

Aşağıdaki sorguda da GetAncestor kullanılır, ancak bu kez, bu halflingin adı göz önüne alındığında, halflingin doğrudan soyundan gelenlerini bulmak için.

IQueryable<Halfling> FindDirectDescendents(string name)
    => context.Halflings.Where(
        descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
            .Single(ancestor => ancestor.Name == name).PathFromPatriarch);

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0)

"Halfling 'Mungo' sorgusu çalıştırıldığında, 'Bungo', 'Belba', 'Longo' ve 'Linda' döndürülüyor."

Bir varlığın tüm atalarını alma

GetAncestor tek bir seviyeyi yukarı veya aşağı aramak ya da belirli bir sayıda seviyeyi aramak için kullanışlıdır. Öte yandan, IsDescendantOf tüm ataları veya bağımlıları bulmak için yararlıdır. Örneğin, aşağıdaki sorgu, bir yarımelfin adını kullanarak o yarımelfin tüm atalarını bulmak için IsDescendantOf kullanır:

IQueryable<Halfling> FindAllAncestors(string name)
    => context.Halflings.Where(
            ancestor => context.Halflings
                .Single(
                    descendent =>
                        descendent.Name == name
                        && ancestor.Id != descendent.Id)
                .PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
        .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel());

Important

IsDescendantOf kendisi için true değerini döndürür. Bu nedenle yukarıdaki sorguda filtrelenmiştir.

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

"Hobbit 'Bilbo' için bu sorgu çalıştırıldığında 'Bungo', 'Mungo' ve 'Balbo' döndürülüyor."

Bir varlığın tüm alt öğelerini al

Aşağıdaki sorguda da kullanılır IsDescendantOf, ancak bu kez bu yarımlamanın adı göz önünde bulundurulduğunda bir yarımlamanın tüm alt öğelerine kullanılır:

IQueryable<Halfling> FindAllDescendents(string name)
    => context.Halflings.Where(
            descendent => descendent.PathFromPatriarch.IsDescendantOf(
                context.Halflings
                    .Single(
                        ancestor =>
                            ancestor.Name == name
                            && descendent.Id != ancestor.Id)
                    .PathFromPatriarch))
        .OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].IsDescendantOf((
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel()

"Mungo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho" ve "Poppy" döndürülüyor.

Ortak bir ata bulma

Bu aile ağacı hakkında sorulan en yaygın sorulardan biri, "Frodo ve Bilbo'nun ortak atası kim?" sorusudur. Böyle bir sorgu yazmak için kullanabiliriz IsDescendantOf :

async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)
    => await context.Halflings
        .Where(
            ancestor => first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
                        && second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
        .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel())
        .FirstOrDefaultAsync();

Bu, aşağıdaki SQL'e çevrilir:

SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
  AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

Bu sorguyu "Bilbo" ve "Frodo" ile çalıştırmak, ortak atalarının "Balbo" olduğunu bildirir.

Hiyerarşileri güncelleştirme

Sütunları güncelleştirmek için normal değişiklik izleme ve SaveChanges mekanizmaları hierarchyid kullanılabilir.

Alt hiyerarşinin üst öğesini yeniden belirleme

Örneğin, DNA testinde Longo'nun aslında Mungo'nun değil aslında Ponto'nun oğlu olduğunu ortaya çıkardığında hepimizin SR 1752 (yani "LongoGate") skandalını hatırlayacağından eminim! Bu skandaldan bir tanesi, aile ağacının yeniden yazılması gerektiğiydi. Özellikle, Longo ve tüm soyundan gelenlerin Mungo'dan Ponto'ya yeniden yönlendirilmesi gerekti. GetReparentedValue bunu yapmak için kullanılabilir. Örneğin, ilk "Longo" ve tüm soyundan gelenler sorgulanır:

var longoAndDescendents = await context.Halflings.Where(
        descendent => descendent.PathFromPatriarch.IsDescendantOf(
            context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
    .ToListAsync();

Ardından GetReparentedValue Longo ve her alt öğesi için kullanılır, HierarchyId güncellendikten sonra SaveChangesAsync öğesine bir çağrı yapılır.

foreach (var descendent in longoAndDescendents)
{
    descendent.PathFromPatriarch
        = descendent.PathFromPatriarch.GetReparentedValue(
            mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}

await context.SaveChangesAsync();

Bu, aşağıdaki veritabanı güncelleştirmesine neden olur:

SET NOCOUNT ON;
UPDATE [Halflings] SET [PathFromPatriarch] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Halflings] SET [PathFromPatriarch] = @p2
OUTPUT 1
WHERE [Id] = @p3;
UPDATE [Halflings] SET [PathFromPatriarch] = @p4
OUTPUT 1
WHERE [Id] = @p5;

Şu parametreleri kullanarak:

 @p1='9',
 @p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
 @p3='16',
 @p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
 @p5='23',
 @p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)

Note

Özelliklerin parametre değerleri HierarchyId veritabanına kompakt, ikili biçimlerinde gönderilir.

Güncellemeden sonra "Mungo" alt öğeleri sorgulandığında "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco" ve "Poppy" döndürülürken, "Ponto" alt öğeleri sorgulandığında "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony" ve "Angelica" döndürülür.

İşlev eşlemeleri

.NET SQL
hierarchyId.GetAncestor(n) @hierarchyId.GetAncestor(@n)
hierarchyId.GetDescendant(child) @hierarchyId.GetDescendant(@child, NULL)
hierarchyId.GetDescendant(çocuk1, çocuk2) @hierarchyId.GetDescendant(@child1, @child2)
hierarchyId.GetLevel() @hierarchyId.GetLevel()
hierarchyId.GetReparentedValue(oldRoot, newRoot) metodu, eski kökü (oldRoot) yeni köke (newRoot) yeniden yönlendirir. @hierarchyId.GetReparentedValue(@oldRoot, @newRoot)
HierarchyId.GetRoot() hierarchyid::GetRoot()
hierarchyId.IsDescendantOf(parent) @hierarchyId.IsDescendantOf(@parent)
HierarchyId.Parse(input) hierarchyid::Parse(@input)
hierarchyId.ToString() @hierarchyId.ToString()

Ek kaynaklar