SQL Server EF Core プロバイダーでの階層データ
Note
この機能は、EF Core 8.0 で追加されました。
Azure SQL と SQL Server には、hierarchyid
を格納するために使われる という特殊なデータ型があります。 この場合、"階層データ" とは、基本的に、各項目が親や子を持つことができるツリー構造を形成するデータのことです。 このようなデータの例を次に示します。
- 組織構造
- ファイル システム
- プロジェクト内のタスクのセット
- 言語の用語の分類
- Web ページ間のリンクのグラフ
この場合、データベースは、このデータに対してその階層構造を使ってクエリを実行できます。 たとえば、クエリを使って特定の項目の先祖と依存関係を見つけることや、階層の特定の深さにあるすべての項目を見つけることができます。
.NET と EF Core での HierarchyId の使用
最下位レベルでは、 Microsoft.SqlServer.Types NuGet パッケージには、 SqlHierarchyId
という型が含まれています。 この型は動作する hierarchyid 値をサポートしていますが、LINQ で使用するには少し面倒です。
次のレベルでは、新しい Microsoft.EntityFrameworkCore.SqlServer.Abstractions パッケージが導入されました。これには、エンティティ型での使用を目的とした上位レベルの HierarchyId
型が含まれています。
ヒント
.NET Framework 型が SQL Server データベース エンジン内でホストされる方法に基づいてモデル化された HierarchyId
よりも、 SqlHierarchyId
型は .NET の規範に沿っています。 HierarchyId
は EF Core で機能するように設計されていますが、EF Core の外部でも、他のアプリケーションで使うことができます。 Microsoft.EntityFrameworkCore.SqlServer.Abstractions
パッケージは他のパッケージを参照しないため、展開されたアプリケーションのサイズと依存関係への影響は最小限に抑えられます。
クエリや更新などの EF Core 機能に HierarchyId
を使うには 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 は、これらのパスにコンパクトなバイナリ形式を使用しますが、コードを操作するときに人間が判読できる文字列表現との間で解析するのが一般的です。 この表現では、各レベルの位置は /
文字で区切られます。 たとえば、次の図の家系図を考えてみましょう。
このツリーの内容:
- ツリーのルートには Balbo がおり、
/
で表されます。 - Balbo には 5 人の子供がおり、
/1/
、/2/
、/3/
、/4/
、/5/
で表されます。 - Balbo の最初の子供である Mungo にも、
/1/1/
、/1/2/
、/1/3/
、/1/4/
、/1/5/
で表される 5 人の子供がいます。HierarchyId
for Mungo (/1/
) は、そのすべての子の接頭辞である点に注意してください。 - 同様に、Balbo の 3 人目の子供である Ponto には
/3/1/
と/3/2/
で表される 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();
ヒント
必要に応じて、10 進数値を使って 2 つの既存のノード間に新しいノードを作成できます。 たとえば、 /3/2.5/2/
と /3/2/2/
の間に /3/3/2/
を挿入します。
階層のクエリ
HierarchyId
は、LINQ クエリで使用できるいくつかのメソッドを公開しています。
Method | 説明 |
---|---|
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
は、1 レベル、または指定した数のレベル分、上または下に検索する場合に便利です。 一方、 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" が返されます。
共通の祖先を見つける
この特定の家系図についてよく聞かれる質問の 1 つは、"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
列を更新できます。
サブ階層の再ペアレンティング
たとえば、SR 1752 (通称 "LongoGate") のスキャンダルは皆さん覚えていますよね。Longo が実は Mungo の息子ではなく、Ponto の息子であることが DNA 鑑定で明らかになったというものです。 このスキャンダルによって、家系図を書き直す必要が生じました。 特に、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)
Note
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.GetLevel() | @hierarchyId.GetLevel() |
hierarchyId.GetReparentedValue(oldRoot, newRoot) | $ |
HierarchyId.GetRoot() | hierarchyid::GetRoot() |
hierarchyId.IsDescendantOf(parent) | @hierarchyId.IsDescendantOf(@parent) |
HierarchyId.Parse(input) | hierarchyid::Parse(@input) |
hierarchyId.ToString() | @hierarchyId.ToString() |
その他のリソース
.NET