Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Note
Эта функция была добавлена в EF Core 8.0.
Sql Azure и SQL Server имеют специальный тип hierarchyid данных, который используется для хранения иерархических данных. В этом случае "иерархические данные" по сути означают данные, формирующие структуру дерева, где каждый элемент может иметь родительский элемент и/или дочерние элементы. Примерами таких данных являются:
- Организационная структура
- Файловая система
- группа задач в проекте;
- Классификация языковых терминов
- Диаграмма связей между веб-страницами
Затем база данных сможет выполнять запросы к этим данным с помощью иерархической структуры. Например, запрос может найти предков и зависимых от заданных элементов или найти все элементы в определенной глубине иерархии.
Использование HierarchyId в .NET и EF Core
На самом низком уровне пакет NuGet Microsoft.SqlServer.Types включает тип SqlHierarchyId. Хотя этот тип поддерживает рабочие значения иерархии, это немного сложно для работы с LINQ.
На следующем уровне появился новый пакет Microsoft.EntityFrameworkCore.SqlServer.Abstractions , который включает более высокий HierarchyId тип, предназначенный для использования в типах сущностей.
Tip
Тип HierarchyId является более идиоматичным для норм .NET, в отличие от SqlHierarchyId, который смоделирован по принципу того, как типы .NET Framework размещаются в ядре СУБД SQL Server.
HierarchyId предназначен для работы с EF Core, но его также можно использовать за пределами EF Core в других приложениях. Пакет Microsoft.EntityFrameworkCore.SqlServer.Abstractions не ссылается на другие пакеты и поэтому имеет минимальное влияние на развернутый размер приложения и зависимости.
HierarchyId Для использования функций EF Core, таких как запросы и обновления, требуется пакет Microsoft.EntityFrameworkCore.SqlServer.HierarchyId. Этот пакет включает Microsoft.EntityFrameworkCore.SqlServer.Abstractions и Microsoft.SqlServer.Types как транзитивные зависимости, поэтому это часто единственный необходимый пакет.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.HierarchyId
После установки пакета использование HierarchyId включается путем вызова UseHierarchyId в рамках вызова UseSqlServer приложения. Рассмотрим пример.
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
Иерархии моделирования
Тип HierarchyId можно использовать для свойств типа сущности. Например, предположим, что мы хотим смоделировать родословное дерево по отцовской линии некоторых вымышленных хоббитов. В типе сущности для Halfling свойство HierarchyId можно использовать для поиска каждого полурослика в семействе.
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
Код, показанный здесь, и в приведенных ниже примерах происходит из HierarchyIdSample.cs.
Tip
При необходимости HierarchyId подходит для использования в качестве типа свойства ключа.
В этом случае семейное дерево начинается с патриарха семьи. Каждого халфлинга можно проследить от патриарха вниз по дереву, используя его PathFromPatriarch свойство. SQL Server использует компактный двоичный формат для этих путей, но при работе с кодом обычно выполняется синтаксический анализ удобочитаемого строкового представления. В этом представлении позиция на каждом уровне разделена символом / . Например, рассмотрим дерево семьи на схеме ниже:
В этом дереве:
- Балбо находится в корне дерева, представленного
/. - Балбо имеет пять детей, представленных
/1/,/2/,/3/,/4/и/5/. - Первый ребенок Балбо, Мунго, также имеет пять детей, представленных
/1/1/, ,/1/2/,/1/3//1/4/и/1/5/. Обратите внимание, чтоHierarchyId, относящийся к Мунго (/1/), является префиксом для всех его детей. - Аналогичным образом третий ребенок Балбо, Понто, имеет двух детей, представленных
/3/1/и/3/2/. Снова каждый из этих потомков получает префиксHierarchyIdдля Ponto, который представлен как/3/. - И так далее вниз по дереву...
Следующий код вставляет это дерево семейства в базу данных с помощью 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
При необходимости десятичные значения можно использовать для создания новых узлов между двумя существующими узлами. Например, /3/2.5/2/ идет между /3/2/2/ и /3/3/2/.
Запросы в иерархиях
HierarchyId предоставляет несколько методов, которые можно использовать в запросах LINQ.
| Method | Description |
|---|---|
GetAncestor(int n) |
Возвращает узел n на заданное количество уровней вверх по иерархическому дереву. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
Возвращает значение узла-потомка, которое больше, чем child1, и меньше, чем child2. |
GetLevel() |
Возвращает уровень этого узла в иерархическом дереве. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Получает значение, отражающее местоположение нового узла, имеющего путь от newRoot, который совпадает с путем от oldRoot до данного узла, эффективно перемещая его в новое местоположение. |
IsDescendantOf(HierarchyId? parent) |
Возвращает значение, указывающее, является ли этот узел потомком parent. |
Кроме того, операторы ==, !=, <<=> и >= можно использовать.
Ниже приведены примеры использования этих методов в запросах LINQ.
Получение сущностей на заданном уровне в дереве
Следующий запрос с использованием GetLevel возвращает всех полуросликов на заданном уровне в семейном дереве:
var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
Это преобразуется в следующий SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
Запустив это в цикле, мы можем получить полуросликов для каждого поколения.
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
Получение прямого предка сущности
Следующий запрос использует GetAncestor для поиска прямого предка полурослика по его имени:
async Task<Halfling?> FindDirectAncestor(string name)
=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name == name).PathFromPatriarch
.GetAncestor(1));
Это преобразуется в следующий SQL:
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" возвращает "Bungo".
Получите прямых потомков сущности
Следующий запрос также использует GetAncestor, но на этот раз для поиска прямых потомков полурослика, учитывая имя этого полурослика.
IQueryable<Halfling> FindDirectDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);
Это преобразуется в следующий SQL:
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" возвращает "Bungo", "Белба", "Лонго" и "Линда".
Получите всех предков сущности
GetAncestor полезен для поиска на один уровень вверх или вниз, или на указанное количество уровней. С другой стороны, IsDescendantOf полезен для нахождения всех предков или зависимых элементов. Например, следующий запрос использует 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());
Important
IsDescendantOf возвращает значение true для себя, поэтому оно отфильтровывается в приведенном выше запросе.
Это преобразуется в следующий SQL:
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" возвращает "Bungo", "Mungo" и "Balbo".
Получите всех потомков сущности
Следующий запрос также использует IsDescendantOf, но на этот раз для всех потомков полурослика, учитывая имя этого полурослика.
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());
Это преобразуется в следующий SQL:
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" возвращает "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho" и "Poppy".
Поиск общего предка
Одним из наиболее распространенных вопросов, задаваемых об этом конкретном дереве семьи, является "кто общий предок Фродо и Бильбо?" Мы можем использовать 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();
Это преобразуется в следующий SQL:
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
Выполнение этого запроса с "Bilbo" и "Frodo" показывает нам, что их общий предок — "Balbo".
Обновление иерархий
Обычные механизмы отслеживания изменений и SaveChanges можно использовать для обновления hierarchyid столбцов.
Повторное создание родительского элемента в под иерархии
Например, я уверен, что мы все помним скандал SR 1752 (также известный как "LongoGate"), когда тестирование ДНК показало, что Лонго не являлся сыном Мунго, а был сыном Понто! Одним из последствий этого скандала было то, что семейное дерево должно быть переписано. В частности, Лонго и все его потомки должны быть переназначены от Мунго к Понто.
GetReparentedValue это можно использовать для этого. Например, сначала запрашиваются "Longo" и все его потомки:
var longoAndDescendents = await context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
.ToListAsync();
Затем GetReparentedValue используется для обновления HierarchyId для Longo и каждого его потомка, после чего вызывается SaveChangesAsync:
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
Это приводит к следующему обновлению базы данных:
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;
Использование следующих параметров:
@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
Значения параметров для HierarchyId свойств отправляются в базу данных в их компактном двоичном формате.
После обновления запрос потомков "Мунго" возвращает "Бунго", "Белба", "Линда", "Бинго", "Бильбо", "Фалько" и "Поппи", при запросе потомков "Понто" возвращается "Лонго", "Роза", "Поло", "Ото", "Поско", "Приска", "Лото", "Понто", "Порто", "Пиони" и "Анжелика".
Сопоставления функций
| .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::Parse(@input) |
| hierarchyId.ToString() | @hierarchyId.ToString() |