Data Hierarkis di Penyedia Inti EF SQL Server
Catatan
Fitur ini ditambahkan dalam EF Core 8.0.
Azure SQL dan SQL Server memiliki jenis data khusus yang disebut hierarchyid
yang digunakan untuk menyimpan data hierarkis. Dalam hal ini, "data hierarkis" pada dasarnya berarti data yang membentuk struktur pohon, di mana setiap item dapat memiliki induk dan/atau anak. Contoh data tersebut adalah:
- Struktur organisasi
- Sistem file
- Sekumpulan tugas dalam proyek
- Taksonomi istilah bahasa
- Grafik tautan antar halaman Web
Database kemudian dapat menjalankan kueri terhadap data ini menggunakan struktur hierarkisnya. Misalnya, kueri dapat menemukan leluhur dan dependen item tertentu, atau menemukan semua item pada kedalaman tertentu dalam hierarki.
Menggunakan HierarchyId di .NET dan EF Core
Pada tingkat terendah, paket NuGet Microsoft.SqlServer.Type menyertakan jenis yang disebut SqlHierarchyId
. Meskipun jenis ini mendukung nilai hierarkis yang berfungsi, agak rumit untuk dikerjakan di LINQ.
Pada tingkat berikutnya, paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions baru telah diperkenalkan, yang mencakup jenis tingkat HierarchyId
yang lebih tinggi yang dimaksudkan untuk digunakan dalam jenis entitas.
Tip
Jenis HierarchyId
ini lebih idiomatik dengan norma .NET daripada SqlHierarchyId
, yang sebaliknya dimodelkan setelah bagaimana jenis .NET Framework dihosting di dalam mesin database SQL Server.
HierarchyId
dirancang untuk bekerja dengan EF Core, tetapi juga dapat digunakan di luar EF Core di aplikasi lain. Paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions
ini tidak mereferensikan paket lain, sehingga berdampak minimal pada ukuran dan dependensi aplikasi yang disebarkan.
Penggunaan HierarchyId
untuk fungsionalitas EF Core seperti kueri dan pembaruan memerlukan paket Microsoft.EntityFrameworkCore.SqlServer.HierarchyId . Paket ini membawa dan Microsoft.EntityFrameworkCore.SqlServer.Abstractions
Microsoft.SqlServer.Types
sebagai dependensi transitif, dan begitu juga seringkali satu-satunya paket yang diperlukan.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.HierarchyId
Setelah paket diinstal, penggunaan HierarchyId
diaktifkan dengan memanggil UseHierarchyId
sebagai bagian dari panggilan aplikasi ke UseSqlServer
. Contohnya:
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
Hierarki pemodelan
Jenis HierarchyId
dapat digunakan untuk properti jenis entitas. Misalnya, asumsikan kita ingin memodelkan pohon keluarga paternal dari beberapa paruh fiksi. Dalam jenis entitas untuk Halfling
, HierarchyId
properti dapat digunakan untuk menemukan setiap setengah di pohon keluarga.
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
Kode yang ditunjukkan di sini dan dalam contoh di bawah ini berasal dari HierarchyIdSample.cs.
Tip
Jika diinginkan, HierarchyId
cocok untuk digunakan sebagai jenis properti kunci.
Dalam hal ini, pohon keluarga berakar pada patriarki keluarga. Setiap halfling dapat dilacak dari patriarki di bawah pohon menggunakan propertinya PathFromPatriarch
. SQL Server menggunakan format biner yang ringkas untuk jalur ini, tetapi umum untuk mengurai jalur tersebut ke dan dari representasi string yang dapat dibaca manusia saat bekerja dengan kode. Dalam representasi ini, posisi di setiap tingkat dipisahkan oleh /
karakter. Misalnya, pertimbangkan pohon keluarga dalam diagram di bawah ini:
Di pohon ini:
- Balbo berada di akar pohon, diwakili oleh
/
. - Balbo memiliki lima anak, yang diwakili oleh
/1/
, ,/2/
/3/
,/4/
, dan/5/
. - Anak pertama Balbo, Mungo, juga memiliki lima anak, yang diwakili oleh
/1/1/
, ,/1/2/
/1/3/
,/1/4/
, dan/1/5/
. Perhatikanlah bahwaHierarchyId
untuk Mungo (/1/
) adalah awalan untuk semua anak-anaknya. - Demikian pula, anak ketiga Balbo, Ponto, memiliki dua anak, diwakili oleh
/3/1/
dan/3/2/
. Sekali lagi masing-masing anak ini diawali olehHierarchyId
untuk Ponto, yang diwakili sebagai/3/
. - Dan sebagainya di bawah pohon ...
Kode berikut menyisipkan pohon keluarga ini ke dalam database menggunakan EF Core:
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
Jika diperlukan, nilai desimal dapat digunakan untuk membuat simpul baru di antara dua simpul yang ada. Misalnya, /3/2.5/2/
berjalan antara /3/2/2/
dan /3/3/2/
.
Mengkueri hierarki
HierarchyId
mengekspos beberapa metode yang dapat digunakan dalam kueri LINQ.
Metode | Deskripsi |
---|---|
GetAncestor(int n) |
Mendapatkan tingkat simpul n ke atas pohon hierarkis. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
Mendapatkan nilai node turunan yang lebih besar dari child1 dan kurang dari child2 . |
GetLevel() |
Mendapatkan tingkat simpul ini di pohon hierarkis. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Mendapatkan nilai yang mewakili lokasi simpul baru yang memiliki jalur dari newRoot sama dengan jalur dari oldRoot ke ini, secara efektif memindahkan ini ke lokasi baru. |
IsDescendantOf(HierarchyId? parent) |
Mendapatkan nilai yang menunjukkan apakah simpul ini adalah turunan dari parent . |
Selain itu, operator ==
, , !=
<
, <=
, >
dan >=
dapat digunakan.
Berikut ini adalah contoh penggunaan metode ini dalam kueri LINQ.
Mendapatkan entitas pada tingkat tertentu di pohon
Kueri berikut menggunakan GetLevel
untuk mengembalikan semua halfling pada tingkat tertentu di pohon keluarga:
var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
Ini diterjemahkan ke SQL berikut:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
Menjalankan ini dalam perulangan kita bisa mendapatkan halfling untuk setiap generasi:
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
Mendapatkan leluhur langsung entitas
Kueri berikut menggunakan GetAncestor
untuk menemukan leluhur langsung dari halfling, mengingat nama halfling tersebut:
async Task<Halfling?> FindDirectAncestor(string name)
=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name == name).PathFromPatriarch
.GetAncestor(1));
Ini diterjemahkan ke SQL berikut:
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)
Menjalankan kueri ini untuk halfling "Bilbo" mengembalikan "Bungo".
Mendapatkan turunan langsung entitas
Kueri berikut juga menggunakan GetAncestor
, tetapi kali ini untuk menemukan turunan langsung dari halfling, mengingat nama halfling tersebut:
IQueryable<Halfling> FindDirectDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);
Ini diterjemahkan ke SQL berikut:
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)
Menjalankan kueri ini untuk halfling "Mungo" mengembalikan "Bungo", "Belba", "Longo", dan "Linda".
Mendapatkan semua leluhur entitas
GetAncestor
berguna untuk mencari ke atas atau ke bawah satu tingkat, atau, memang, jumlah tingkat yang ditentukan. Di sisi lain, IsDescendantOf
berguna untuk menemukan semua leluhur atau dependen. Misalnya, kueri berikut menggunakan IsDescendantOf
untuk menemukan semua leluhur dari halfling, mengingat nama halfling itu:
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());
Penting
IsDescendantOf
mengembalikan true untuk dirinya sendiri, itulah sebabnya difilter dalam kueri di atas.
Ini diterjemahkan ke SQL berikut:
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
Menjalankan kueri ini untuk halfling "Bilbo" mengembalikan "Bungo", "Mungo", dan "Balbo".
Mendapatkan semua turunan entitas
Kueri berikut juga menggunakan IsDescendantOf
, tetapi kali ini untuk semua turunan dari halfling, mengingat nama halfling itu:
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());
Ini diterjemahkan ke SQL berikut:
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()
Menjalankan kueri ini untuk halfling "Mungo" mengembalikan "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho", dan "Poppy".
Menemukan leluhur umum
Salah satu pertanyaan paling umum yang diajukan tentang pohon keluarga khusus ini adalah, "siapa nenek moyang umum Frodo dan Bilbo?" Kita dapat menggunakan IsDescendantOf
untuk menulis kueri seperti itu:
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();
Ini diterjemahkan ke SQL berikut:
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
Menjalankan kueri ini dengan "Bilbo" dan "Frodo" memberi tahu kita bahwa leluhur umum mereka adalah "Balbo".
Memperbarui hierarki
Mekanisme pelacakan perubahan normal dan SaveChanges dapat digunakan untuk memperbarui hierarchyid
kolom.
Mengasuh ulang sub-hierarki
Misalnya, saya yakin kita semua ingat skandal SR 1752 (alias "LongoGate") ketika pengujian DNA mengungkapkan bahwa Longo sebenarnya bukan putra Mungo, tetapi sebenarnya putra Ponto! Satu fallout dari skandal ini adalah bahwa pohon keluarga perlu ditulis ulang. Secara khusus, Longo dan semua keturunannya perlu diasuh kembali dari Mungo ke Ponto.
GetReparentedValue
dapat digunakan untuk melakukan ini. Misalnya, "Longo" pertama dan semua keturunannya dikueri:
var longoAndDescendents = await context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
.ToListAsync();
Kemudian GetReparentedValue
digunakan untuk memperbarui HierarchyId
untuk Longo dan setiap keturunan, diikuti dengan panggilan ke SaveChangesAsync
:
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
Ini menghasilkan pembaruan database berikut:
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;
Menggunakan parameter ini:
@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)
Catatan
Nilai parameter untuk HierarchyId
properti dikirim ke database dalam format biner yang ringkas.
Setelah pembaruan, mengkueri turunan "Mungo" mengembalikan "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco", dan "Poppy", sementara mengkueri keturunan "Ponto" mengembalikan "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony", dan "Angelica".
Pemetaan fungsi
.NET | SQL |
---|---|
hierarchyId.GetAncestor(n) | @hierarchyId.GetAncestor(@n) |
hierarchyId.GetDescendant(child) | @hierarchyId.GetDescendant(@child, NULL) |
hierarchyId.GetDescendant(child1, child2) | @hierarchyId.GetDescendant(@child1, @child2) |
hierarchyId.GetLevel() | @hierarchyId.GetLevel() |
hierarchyId.GetReparentedValue(oldRoot, newRoot) | @hierarchyId.GetReparentedValue(@oldRoot, @newRoot) |
HierarchyId.GetRoot() | hierarchyid::GetRoot() |
hierarchyId.IsDescendantOf(parent) | @hierarchyId.IsDescendantOf(@parent) |
HierarchyId.Parse(input) | hierarchyid::P arse(@input) |
hierarchyId.ToString() | @hierarchyId.ToString() |