SQL Server EF Core Sağlayıcısında Hiyerarşik Veriler
Not
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ı SqlHierarchyId
bir tür içerir. Bu tür çalışma hiyerarşisi değerlerini desteklese de LINQ ile çalışmak biraz zahmetlidir.
Sonraki düzeyde, varlık türlerinde kullanılması amaçlanan daha üst düzey bir tür içeren yeni HierarchyId
paketi kullanıma sunulmuştur.
İpucu
Türü HierarchyId
, .NET'in normlarına göre daha SqlHierarchyId
idiyomatiktir ve bunun yerine .NET Framework türlerinin SQL Server veritabanı altyapısında barındırıldığı şekilde modellenir.
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
sorgular ve güncelleştirmeler gibi EF Core işlevleri için kullanımı Için Microsoft.EntityFrameworkCore.SqlServer.HierarchyId paketi gerekir. Bu paket ve öğesini geçişli bağımlılıklar olarak getirir Microsoft.EntityFrameworkCore.SqlServer.Abstractions
Microsoft.SqlServer.Types
ve genellikle gereken tek paket de budur.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.HierarchyId
Paket yüklendikten sonra, uygulamasının HierarchyId
çağrısının bir parçası olarak çağrısı UseHierarchyId
UseSqlServer
yapılarak kullanımı etkinleştirilir. Ö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. için Halfling
varlık türünde, aile ağacındaki her yarıyı bulmak için bir HierarchyId
özellik 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; }
}
İpucu
Burada ve aşağıdaki örneklerde gösterilen kod HierarchyIdSample.cs...
İpucu
İ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:
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. Tüm çocuklarının ön ekininHierarchyId
Mungo (/1/
) için olduğuna dikkat edin. - Benzer şekilde Balbo'nun üçüncü çocuğu Ponto'nun ve
/3/1/
ile/3/2/
temsil edilen iki çocuğu vardır. Yine bu çocukların her biri, olarak temsil edilenHierarchyId
Ponto için ön ekini/3/
alır. - Ve ağacın aşağısı...
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();
İpucu
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/
gider.
Hiyerarşileri sorgulama
HierarchyId
LINQ sorgularında kullanılabilecek çeşitli yöntemleri kullanıma sunar.
Metot | Açıklama |
---|---|
GetAncestor(int n) |
Hiyerarşik ağaçta düğüm n düzeylerini alır. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
değerinden büyük child1 ve child2 küçük bir alt düğümün değerini alır. |
GetLevel() |
Hiyerarşik ağaçta bu düğümün düzeyini alır. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Bu konumdan buna eşit newRoot bir yola oldRoot sahip yeni düğümün konumunu temsil eden bir değer alır ve bunu yeni konuma etkili bir şekilde 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 belirli bir düzeydeki varlıkları alma
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ı alma
Aşağıdaki sorgu, bu yarımlamanın adı göz önünde bulundurularak bir yarılanmanın doğrudan atası bulmak için kullanır GetAncestor
:
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)
"Bilbo" yarımlama için bu sorgu çalıştırılırken "Bungo" döndürülüyor.
Bir varlığın doğrudan alt öğelerini alma
Aşağıdaki sorguda da kullanılır GetAncestor
, ancak bu kez bu yarımlamanın adı göz önüne alındığında, bir yarıya ait doğrudan alt öğeleri bulmak için kullanılır:
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)
"Mungo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Belba", "Longo" ve "Linda" döndürülüyor.
Bir varlığın tüm atalarını alma
GetAncestor
tek bir düzeyi veya gerçekten de belirli sayıda düzeyi 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, bu yarımlamanın adı göz önünde bulundurularak bir yarımlamanın tüm atalarını bulmak için kullanır IsDescendantOf
:
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());
Önemli
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
"Bilbo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Mungo" ve "Balbo" döndürülüyor.
Bir varlığın tüm alt öğelerini alma
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şiyi yeniden üst öğe oluşturma
Ö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 ebeveyn olması gerekiyordu.
GetReparentedValue
bunu yapmak için kullanılabilir. Örneğin, ilk "Longo" ve tüm alt öğeleri 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 öğesini güncelleştirmek HierarchyId
için kullanılır ve ardından öğesine bir çağrı SaveChangesAsync
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)
Not
Özelliklerin parametre değerleri HierarchyId
veritabanına kompakt, ikili biçimlerinde gönderilir.
Güncelleştirmeden sonra "Mungo" alt öğeleri sorgulanması "Bungo" döndürür, "Belba", "Linda", "Bingo", "Bilbo", "Falco" ve "Poppy", "Ponto" öğesinin alt öğeleri için sorgu yapılırken "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony" ve "Angelica" döndürür.
İşlev eşlemeleri
.NET | SQL |
---|---|
hierarchyId.GetAncestor(n) | @hierarchyId.GetAncestor(@n) |
hierarchyId.GetDescendant(alt) | @hierarchyId.GetDescendant(@child, NULL) |
hierarchyId.GetDescendant(alt1, alt2) | @hierarchyId.GetDescendant(@child1, @child2) |
hierarchyId.GetLevel() | @hierarchyId.GetLevel() |
hierarchyId.GetReparentedValue(oldRoot, newRoot) | @hierarchyId.GetReparentedValue(@oldRoot, @newRoot) |
HierarchyId.GetRoot() | hierarchyid::GetRoot() |
hierarchyId.IsDescendantOf(üst) | @hierarchyId.IsDescendantOf(@parent) |
HierarchyId.Parse(input) | hierarchyid::P arse(@input) |
hierarchyId.ToString() | @hierarchyId.ToString() |