SQL Server EF Core 提供程序中的分层数据

注意

EF Core 8.0 中添加了此功能。

Azure SQL 和 SQL Server 具有一个名为 hierarchyid 的特殊数据类型,用于存储分层数据。 在这种情况下,“分层数据”实质上是指形成树结构的数据,其中每个项都可以有父级和/或子级。 此类数据的示例包括:

  • 组织结构
  • 文件系统
  • 项目中的一组任务
  • 语言术语分类
  • 网页间链接图

然后,数据库可以使用其分层结构对此数据运行查询。 例如,查询可以查找给定项的上级和依赖项,或查找层次结构中某个深度的所有项。

在 .NET 和 EF Core 中使用 HierarchyId

在最低级别,Microsoft.SqlServer.Types NuGet 包包含一个名为 SqlHierarchyId 的类型。 虽然此类型支持工作 hierarchyid 值,但在 LINQ 中使用它有点麻烦。

在下一个级别,引入了新的 Microsoft.EntityFrameworkCore.SqlServer.Abstractions 包,其中包括用于实体类型的高级 HierarchyId 类型。

提示

HierarchyId 类型更习惯于 .NET 标准,而不是 SqlHierarchyId 标准,后者是在 SQL Server 数据库引擎中托管 .NET Framework 类型之后建模的。 HierarchyId 设计为与 EF Core 结合使用,但它也可以在其他应用程序中的 EF Core 外部使用。 Microsoft.EntityFrameworkCore.SqlServer.Abstractions 包不引用任何其他包,因此对部署的应用程序大小和依赖项的影响最小。

对查询和更新等 EF Core 功能使用 HierarchyId 需要 Microsoft.EntityFrameworkCore.SqlServer.HierarchyId 包。 此包将 Microsoft.EntityFrameworkCore.SqlServer.AbstractionsMicrosoft.SqlServer.Types 作为可传递依赖项引入,因此通常是唯一需要的包。

dotnet add package Microsoft.EntityFrameworkCore.SqlServer.HierarchyId

安装包后,通过调用 UseHierarchyId 作为应用程序对 UseSqlServer 的调用的一部分来使用 HierarchyId。 例如:

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 对这些路径使用压缩的二进制格式,但在使用代码时,通常要分析到用户可读的字符串表示形式或从中分析。 在此表示形式中,每个级别的位置由 / 字符分隔。 例如,请考虑下图中的家谱:

Halfling family tree

在此树中:

  • Balbo 位于树的根处,由 / 表示。
  • Balbo 有五个孩子,由 /1//2//3//4//5/ 表示。
  • Balbo 的第一个孩子 Mungo 也有五个孩子,由 /1/1//1/2//1/3//1/4//1/5/ 表示。 请注意,Balbo (/1/) 的 HierarchyId 是其所有孩子的前缀。
  • 同样,Balbo 的第三个孩子 Ponto 有两个孩子,由 /3/1//3/2/ 表示。 同样,其中每个孩子都以 Ponto 的 HierarchyId 为前缀,表示为 /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 查询的多种方法。

方法 说明
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”、“Belba”、“Longo”和“Linda”。

获取实体的所有祖先

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”。

查找共同祖先

关于此特殊家谱的最常见问题之一是,“谁是 Frodo 和 Bilbo 的共同祖先?”可以使用 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 列。

重新设置子层次结构的父级

例如,当 DNA 测试显示,Longo 实际上不是 Mungo 的儿子,而是 Ponto 的儿子时,我确定我们都记得 SR 1752(也称为“LongoGate”)的丑闻! 这场丑闻的一个后果是,家谱需要重新编写。 特别是,Longo 及其所有后代的父系需要从 Mungo 调整为 Ponto。 GetReparentedValue 可用于执行此操作。 例如,查询了第一个“Longo”及其所有后代:

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

然后,GetReparentedValue 用于更新 Longo 和每个子代的 HierarchyId,接着调用 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”、“Belba”、“Linda”、“Bingo”、“Bilbo”、“Falco”、“Poppy”;而查询“Ponto”的后代时将返回“Longo”、“Rosa”、“Polo”、“Otho”、“Posco”、“Prisca”、“Lotho”、“Ponto”、“Porto”、“Peony”、“Angelica”。

函数映射

.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()

其他资源