Spalten mit hierarchyid in Entity Framework Core 8.0 15.04.2024, 00:00 Uhr

Hierarchiedenken

Die aktuelle Version von Entity Framework Core kann auch hierarchische Tabellen im Microsoft SQL Server verwalten.
(Quelle: dotnetpro)
Der Spaltentyp hierarchyid, den es seit Version 2012 in Microsofts Datenbankmanagementsystem SQL Server gibt [1], ist eine spezielle Datenstruktur, die dazu dient, hierarchische Strukturen in relationalen Datenbanken zu repräsentieren und zu verwalten. Dieser Spaltentyp wurde eingeführt, um effiziente Operationen innerhalb von hierarchischen Datenstrukturen zu ermöglichen. Typischerweise wird hierarchyid in Szenarien wie der Organisation von Mitarbeiterhierarchien, Produktkategorien oder genealogischen Daten verwendet.
Jeder Knoten in der Hierarchie wird durch eine eindeutige Hierarchy-ID repräsentiert, wobei es der Anwendung obliegt, die Hierarchy-ID zu vergeben. Die Anwendung übergibt die Hierarchy-ID als Zeichenkette der Form /1/2/3/4/. Mittels der Funktion Parse() erzeugt man aus einer solchen Zeichenkette eine Hierarchy-ID. Der Microsoft SQL Server speichert die Hierarchy-ID in einer internen, binären Datenstruktur mit ­variabler Länge, maximal jedoch 892 Bytes. Die Dokumenta­tion [1] gibt keinen Aufschluss darüber, wie viele Level dies ermöglicht. Sie nennt aber als ein Beispiel, dass man bei 100 000 Datensätzen auf sechs Leveln nur 5 Bytes verbraucht.
Der Spaltentyp hierarchyid ermöglicht effiziente Abfragen und Operationen in hierarchischen Datenstrukturen. Dazu kann der SQL Server die Werte der Hierarchy-ID sortieren und vergleichen. Der Spaltentyp hierarchyid in SQL Server unterstützt das Durchführen von Rekursionen und das Ermitteln von Eltern- und Kindknoten innerhalb einer Hierarchie. Dieser Spaltentyp wird von speziellen Funktionen und Methoden begleitet, die sich auf Hierarchieebenen anwenden lassen. Dazu gehören Funktionen wie GetRoot(), GetLevel(), GetAncestor(), GetDescendant() und IsDescendant­Of().

hierarchyid nun auch in Entity Framework Core

Für die Nutzung von Spalten des Typs hierarchyid innerhalb von Entity Framework Core gab es vor Version 8.0 nur ein ­NuGet-Paket aus der Community: EntityFrameworkCore.SqlServer.HierarchyId [2].
Nun in Entity Framework Core 8.0 hat Microsoft die Unterstützung des Spaltentyps hierarchyid in den Kern von EF Core aufgenommen. Man braucht dafür zwei NuGet-Pakete:
  • Das Paket Microsoft.EntityFrameworkCore.SqlServer.Ab­stractions stellt die Klasse Microsoft.EntityFrameworkCore
    .HierarchyId
    bereit, die man auf der Ebene der Geschäftsobjekte benötigt, um eine Hierarchie zu definieren (siehe die Klasse Mitarbeiter in Listing 1). Ebenso braucht man das Paket dort, wo man LINQ-Befehle auf der HierarchyId absetzen will, zum Beispiel mit den Operationen GetAncestor(), GetDescendant(), IsDescendantOf() und GetLevel(). Diese .NET-Methoden entsprechen den im Microsoft SQL Server verfügbaren Funktionen.
Listing 1: Klasse Mitarbeiter mit HierarchyId
public class Mitarbeiter
{
  public Mitarbeiter(HierarchyId ebene, string name, 
      int? eintrittsjahr = null)
  {
    Ebene = ebene;
    Name = name;
    Eintrittsjahr = eintrittsjahr;
  }
 
  public int ID { get; private set; }
  public HierarchyId Ebene { get; set; }
  public string Name { get; set; }
  public int? Eintrittsjahr { get; set; }
 
  public override string ToString()
  {
    return $"Mitarbeiter {ID} Ebene {Ebene}: {Name}";
  }
}
  • Das Paket Microsoft.EntityFrameworkCore.SqlServer.HierarchyId benötigt man in der Kontextklasse für die Erweiterungsmethode UseHierarchyId().
Die drei Listings des Artikels zeigen ein zusammenhängendes, aussagekräftiges Beispiel für hierarchische Daten mit EF Core 8.0. Die Geschäftsobjektklasse Mitarbeiter in Listing 1 besitzt eine Eigenschaft Ebene vom Typ HierarchyId. Innerhalb von .NET-Code wird diese zum SQL-Server-Spaltentyp hierarchyid (alles in Kleinbuchstaben) gehörende .NET-Klasse mit großem H und I geschrieben: Hie­rarchyId.
Listing 2 realisiert eine EF-Core-Kontextklasse für diese Geschäftsobjektklasse. Dabei wird UseHierarchyId() im zweiten Parameter von UseSqlServer() aufgerufen.
Listing 2: Entity-Framework-Core-Kontextklasse für die Geschäftsobjektklasse Mitarbeiter
class Context : DbContext
{
  public DbSet<Mitarbeiter> MitarbeiterSet { get; set; }
  protected override void OnConfiguring(
      DbContextOptionsBuilder builder)
  {
    builder.EnableSensitiveDataLogging(true);
    builder.UseSqlServer(
      @$"Server=D120;Database=EFC_MappingScenarios_" + 
      nameof(EFC80_HierarchicalData) + 
      ";Trusted_Connection=True;" +
      "MultipleActiveResultSets=True;encrypt=false", 
      x => x.UseHierarchyId());
  }
 
  protected override void OnModelCreating(
      ModelBuilder modelBuilder)
  {
 
  }
}
In Listing 3 findet man einen Client, der zunächst eine Mitarbeiterhierarchie erzeugt. Dazu werden automatisch generierte Testdaten mit den Bibliotheken Bogus und AutoBogus erzeugt, siehe die Instanz der Klasse Faker in Listing 3, die pseudozufällige Namen und Geburtsdaten erzeugt. Details dazu wurden in der Datenzugriffskolumne „Vorgetäuscht“ in dotnetpro 12/2022 [3] erklärt.
Listing 3: Aufbau und Auslesen einer Mitarbeiterhierarchie (Teil 1)
class Client
{
  public void Run()
  {
    CUI.Head(nameof(HierarchicalData));
    using (var ctx = new Context())
    {
      ctx.Database.EnsureCreated();
 
      CUI.H2("Metadaten");
      var model = 
        ctx.GetService<IDesignTimeModel>().Model;
      foreach (var p in model.FindEntityType(typeof(
          Employee)).GetDeclaredProperties())
      {
        Console.WriteLine(p.Name + ": " + 
          p.GetColumnType() + " " + p.GetColumnOrder());
      }
 
      var company = new Company() { 
        Name = "Musterfirma AG" };
 
      #region --------------------------Daten erzeugen
      CUI.H2("\nMitarbeiterhierarchie erzeugen");
      Randomizer.Seed = new Random(42);
      var f = new Faker();
      // Ebene 0
      var startLevel = "/";
      var bosmang = 
        new Employee(HierarchyId.Parse(startLevel), 
        f.Name.FullName(), 
        f.Random.Number(1970, 2023));
      bosmang.Company = company;
      ctx.Add(bosmang);
      var c1 = ctx.SaveChanges();
      Console.WriteLine(bosmang);
 
      // Ebene 1
      for (int i = 1; i < 5; i++)
      {
        var level1 = $"{startLevel}{i}/"; // /1/
 
        var m = new Employee(HierarchyId.Parse(level1), 
          f.Name.FullName(), 
          f.Random.Number(1970, 2023));
        m.Company = company;
        ctx.Add(m);
        var c2 = ctx.SaveChanges();
        Console.WriteLine(" " + m);
 
        // Ebene 2
        for (int j = 1; j < 5; j++)
        {
          var level2 = $"{startLevel}{i}/{j}/"; ;
          var n = new Employee(HierarchyId.Parse(
            level2), f.Name.FullName(), 
            f.Random.Number(1970, 2023));
          n.Company = company;
          ctx.Add(n);
          Console.WriteLine("   " + n.ToString());
          var c3 = ctx.SaveChanges();
        }
      }
      #endregion
 
      #region ------------------------- Daten auslesen
 
      CUI.H2("\nDaten auslesen mit GetLevel() -> " +
        "Level 0 bis 3");
      for (int level = 0; level <= 3; level++)
      {
        var employeOneLevel = 
          ctx.MitarbeiterSet.Where(x => 
          x.Level.GetLevel() == level).ToList();
        CUI.H3($"Mitarbeiter auf Ebene {level}:");
        if (employeOneLevel.Any())
        {
          foreach (var m in employeOneLevel)
          {
            Console.WriteLine(new String(' ', level) + 
              m);
          }
        }
        else
        {
          CUI.Warning("Keine");
        }
      }
 
      CUI.H2("\nAlle untergeordneten Mitarbeiter" + 
        "eines Levels mit IsDescendantOf()");
      var allBelowBosmangBefore = 
        ctx.MitarbeiterSet.Where(x => 
        x.Level.IsDescendantOf(bosmang.Level) && 
        x.Level.GetLevel() == 1).ToList();
      Console.WriteLine($"Alle Mitarbeiter unter " +
        "dem Bosmang (also Level 2):");
      foreach (var m in allBelowBosmangBefore)
      {
        Console.WriteLine(m);
      }
 
      CUI.H2("\nVorgesetzten eines Mitarbeiters" + 
        "auslesen mit GetAncestor()");
      var anyEmployee = 
        ctx.MitarbeiterSet.ToList().ElementAt(15);
      var boss = ctx.MitarbeiterSet.SingleOrDefault(
        ancestor => ancestor.Level == 
        anyEmployee.Level.GetAncestor(1));
 
      Console.WriteLine($"Der Vorgesetzte von " + 
        "{anyEmployee.Name} ist: {boss.Name}");
 
      CUI.H2("\nUntergeordnete Mitarbeiter dieses " +
        "Vorgesetzten auslesen mit IsDescendantOf()");
      var employeesOfOneBoss = 
        ctx.MitarbeiterSet.Where(x => 
        x.Level.IsDescendantOf(boss.Level) && 
        x.ID != boss.ID).ToList();
      Console.WriteLine($"Alle Mitarbeiter von " + 
        "{boss.Name}:");
      foreach (var m in employeesOfOneBoss)
      {
        Console.WriteLine(m);
      }
      #endregion
 
      #region -------------------------- Daten ändern
 
      CUI.H2("\nDaten ändern: Ein Mitarbeiter " +
        "wird befördert und steigt auf die Ebene " +
        "direkt unter dem Bosmang auf, indem wir " +
        "den Level neu setzen");
      Console.WriteLine($"{anyEmployee.Name} - " +
        "Alte Ebene: {anyEmployee.Level}");
      anyEmployee.Level = 
        anyEmployee.Level.GetReparentedValue(
        boss.Level, bosmang.Level);
      Console.WriteLine($"{anyEmployee.Name} - " +
        "Neue Ebene: {anyEmployee.Level}");
      ctx.SaveChanges();
 
      CUI.H2("\nDaten auslesen mit IsDescendantOf()");
      var allBelowBosmangAfter = 
        ctx.MitarbeiterSet.Where(x => 
        x.Level.IsDescendantOf(bosmang.Level) && 
        x.Level.GetLevel() == 1).ToList();
      Console.WriteLine($"Alle Mitarbeiter unter " + 
        "dem Bosmang:");
      foreach (var m in allBelowBosmangAfter)
      {
        Console.WriteLine(m);
      }
      #endregion
    }
  }
}
Der im Listing erwähnte „Bosmang“ ist ein Wort für einen Chef in der Sprache der „Gürtler“ aus der Science-Fiction-Serie „The Expanse“ [4].
Nach dem Erzeugen der Mitarbeiterhierarchie werden verschiedene Abfragen ausgeführt. Am Ende wird auch noch ein Mitarbeiter befördert, das heißt, seine Ebene wird geändert.
Zur besseren Veranschaulichung sieht man in Bild 1 die korrespondierende SQL-Server-Datenbanktabelle, die aus den drei Listings hervorgeht. Bild 2 zeigt die Bildschirmausgabe der Abfragen der hierarchischen Daten.
Entstehende Datenbank mit hierarchyid-Spalte (Bild 1)
Quelle: Autor
Ausgabe des Beispiels in Listing 3 (Bild 2)
Quelle: Autor
Dokumente
Artikel als PDF herunterladen


Das könnte Sie auch interessieren