Иерархические данные в поставщике EF Core SQL Server
Примечание.
Эта функция была добавлена в EF Core 8.0.
Sql Azure и SQL Server имеют специальный тип hierarchyid
данных, который используется для хранения иерархических данных. В этом случае "иерархические данные" по сути означает, что данные, формющие структуру дерева, где каждый элемент может иметь родительский и /или дочерний элемент. Примерами таких данных являются:
- Организационная структура
- Файловая система
- группа задач в проекте;
- Классификация языковых терминов
- Диаграмма связей между веб-страницами
Затем база данных сможет выполнять запросы к этим данным с помощью иерархической структуры. Например, запрос может найти предков и зависимых от заданных элементов или найти все элементы в определенной глубине иерархии.
Использование HierarchyId в .NET и EF Core
На самом низком уровне пакет NuGet Microsoft.SqlServer.Types включает тип SqlHierarchyId
. Хотя этот тип поддерживает рабочие значения иерархии, это немного сложно для работы с LINQ.
На следующем уровне появился новый пакет Microsoft.EntityFrameworkCore.SqlServer.Abstractions , который включает более высокий HierarchyId
тип, предназначенный для использования в типах сущностей.
Совет
Тип 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; }
}
Совет
Код, показанный здесь, и в приведенных ниже примерах происходит из HierarchyIdSample.cs.
Совет
При необходимости 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();
Совет
При необходимости десятичные значения можно использовать для создания новых узлов между двумя существующими узлами. Например, /3/2.5/2/
идет между /3/2/2/
и /3/3/2/
.
Запросы иерархий
HierarchyId
предоставляет несколько методов, которые можно использовать в запросах LINQ.
Метод | 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", "Белба", "Longo" и "Линда".
Получение всех предков сущности
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());
Внимание
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 (a.k.a. "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)
Примечание.
Значения параметров для HierarchyId
свойств отправляются в базу данных в их компактном двоичном формате.
После обновления запрос потомков "Mungo" возвращает "Bungo", "Белба", "Линда", "Бинго", "Bilbo", "Фалько", и "Poppy", при запросе потомков "Понто" возвращается "Лонго", "Роза", "Поло", "Ото", "Posco", "Приска", "Лото", "Понто", "Порто", "Пиони", и "Анжелика".
Сопоставления функций
.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() |