Erweiterte Tabellenzuordnung

EF Core bietet viel Flexibilität bei der Zuordnung von Entitätstypen zu Tabellen in einer Datenbank. Dies wird noch nützlicher, wenn Sie eine Datenbank verwenden müssen, die nicht von EF erstellt wurde.

Die folgenden Techniken werden in Bezug auf Tabellen beschrieben, aber dasselbe Ergebnis kann auch bei der Zuordnung zu Ansichten erreicht werden.

Tabellenaufteilung

EF Core ermöglicht das Zuordnen von zwei oder mehr Entitäten zu einer einzelnen Zeile. Dies wird als Tabellenaufteilung oder Tabellenfreigabebezeichnet.

Konfiguration

Um die Tabellenaufteilung zu verwenden, müssen die Entitätstypen derselben Tabelle zugeordnet, die Primärschlüssel denselben Spalten zugeordnet und mindestens eine Beziehung zwischen dem Primärschlüssel eines Entitätstyps und einem anderen in derselben Tabelle konfiguriert werden.

Ein häufiges Szenario für die Tabellenaufteilung ist die ausschließliche Verwendung einer Teilmenge der Spalten in der Tabelle zur Leistungssteigerung oder Kapselung.

In diesem Beispiel stellt Order eine Teilmenge von DetailedOrder dar.

public class Order
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public string BillingAddress { get; set; }
    public string ShippingAddress { get; set; }
    public byte[] Version { get; set; }
}

Zusätzlich zur erforderlichen Konfiguration rufen wir Property(o => o.Status).HasColumnName("Status") auf, um DetailedOrder.Status derselben Spalte wie Order.Status zuzuordnen.

modelBuilder.Entity<DetailedOrder>(
    dob =>
    {
        dob.ToTable("Orders");
        dob.Property(o => o.Status).HasColumnName("Status");
    });

modelBuilder.Entity<Order>(
    ob =>
    {
        ob.ToTable("Orders");
        ob.Property(o => o.Status).HasColumnName("Status");
        ob.HasOne(o => o.DetailedOrder).WithOne()
            .HasForeignKey<DetailedOrder>(o => o.Id);
        ob.Navigation(o => o.DetailedOrder).IsRequired();
    });

Tipp

Weitere Kontexte finden Sie im vollständigen Beispielprojekt.

Verwendung

Das Speichern und Abfragen von Entitäten mithilfe der Tabellenaufteilung erfolgt auf die gleiche Weise wie bei anderen Entitäten:

using (var context = new TableSplittingContext())
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.Add(
        new Order
        {
            Status = OrderStatus.Pending,
            DetailedOrder = new DetailedOrder
            {
                Status = OrderStatus.Pending,
                ShippingAddress = "221 B Baker St, London",
                BillingAddress = "11 Wall Street, New York"
            }
        });

    context.SaveChanges();
}

using (var context = new TableSplittingContext())
{
    var pendingCount = context.Orders.Count(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"Current number of pending orders: {pendingCount}");
}

using (var context = new TableSplittingContext())
{
    var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}

Optionale abhängige Entität

Wenn alle von einer abhängigen Entität verwendeten Spalten in der Datenbank NULL sind, wird beim Abfragen keine Instanz dafür erstellt. Dies ermöglicht die Modellierung einer optionalen abhängigen Entität, bei der die Beziehungseigenschaft für den Prinzipal NULL wäre. Beachten Sie, dass dies auch der Fall wäre, wenn alle Eigenschaften der abhängigen Entität optional und auf null festgelegt sind, was möglicherweise nicht erwartet wird.

Die zusätzliche Überprüfung kann sich jedoch auf die Abfrageleistung auswirken. Wenn der abhängige Entitätstyp selbst Abhängigkeiten hat, ist die Entscheidung, ob eine Instanz erstellt werden soll, außerdem nicht trivial. Um diese Probleme zu vermeiden, kann der abhängige Entitätstyp als erforderlich gekennzeichnet werden. Weitere Informationen finden Sie unter Erforderliche 1:1-Abhängige.

Parallelitätstoken

Wenn einer der Entitätstypen, die eine Tabelle gemeinsam nutzen, über ein Parallelitätstoken verfügt, muss es auch in alle anderen Entitätstypen eingeschlossen werden. Dies ist notwendig, um einen veralteten Wert für ein Parallelitätstoken zu vermeiden, wenn nur eine der Entitäten, die derselben Tabelle zugeordnet sind, aktualisiert wird.

Um zu vermeiden, dass das Parallelitätstoken für den verarbeitenden Code verfügbar ist, kann ein Token als Schatteneigenschaft erstellt werden:

modelBuilder.Entity<Order>()
    .Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

modelBuilder.Entity<DetailedOrder>()
    .Property(o => o.Version).IsRowVersion().HasColumnName("Version");

Vererbung

Sie sollten die dedizierte Seite zu Vererbung zu lesen, bevor Sie mit diesem Abschnitt fortfahren.

Die abhängigen Typen, die die Tabellenteilung verwenden, können eine Vererbungshierarchie aufweisen, aber es gibt einige Einschränkungen:

  • Der abhängige Entitätstyp kann nicht die TPC-Zuordnung verwenden, da die abgeleiteten Typen nicht derselben Tabelle zugeordnet werden könnten.
  • Der abhängige Entitätstyp kann die TPT-Zuordnung verwenden, aber nur der Stammentitätstyp kann die Tabellenaufteilung verwenden.
  • Wenn der Prinzipalentitätstyp TPC verwendet, können nur die Entitätstypen ohne Nachfolgerelemente die Tabellenaufteilung verwenden. Andernfalls müssten die abhängigen Spalten in den Tabellen dupliziert werden, die den abgeleiteten Typen entsprechen, wobei alle Interaktionen erschwert würden.

Entitätsaufteilung

EF Core ermöglicht das Zuordnen einer Entität zu Zeilen in zwei oder mehr Tabellen. Dies wird als Entitätsaufteilung bezeichnet.

Konfiguration

Betrachten Sie beispielsweise eine Datenbank mit drei Tabellen, die Kundendaten enthalten:

  • Eine Customers Tabelle für Kundeninformationen
  • Eine PhoneNumbers Tabelle für die Telefonnummern der Kunden
  • Eine Addresses-Tabelle für die Kundenadresse

Hier sind Definitionen für diese Tabellen im SQL Server:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

Jede dieser Tabellen wird in der Regel ihrem eigenen Entitätstyp mit Beziehungen zwischen den Typen zugeordnet. Wenn jedoch alle drei Tabellen immer zusammen verwendet werden, kann es praktischer sein, sie einem einzelnen Entitätstyp zuzuordnen. Beispiel:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Dies wird in EF7 erreicht, indem für jede Aufteilung im Entitätstyp SplitToTable aufgerufen wird. Mit dem folgenden Code wird beispielsweise der Entitätstyp Customer auf die oben gezeigten Tabellen Customers, PhoneNumbers und Addresses aufgeteilt:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

Beachten Sie auch, dass bei Bedarf unterschiedliche Spaltennamen für jede der Tabellen angegeben werden können. Informationen zum Konfigurieren des Spaltennamens für die Haupttabelle finden Sie unter Tabellenspezifische Facettenkonfiguration.

Konfigurieren des Verknüpfungs-Fremdschlüssels

Der Fremdschlüssel, der die zugeordneten Tabellen verknüpft, ist auf dieselben Eigenschaften ausgerichtet, für die er deklariert ist. Normalerweise würde er nicht in der Datenbank erstellt werden, da er redundant wäre. Es gibt jedoch eine Ausnahme, wenn der Entitätstyp mehreren Tabellen zugeordnet ist. Um seine Facetten zu ändern, können Sie die Fluent-API zur Beziehungskonfiguration verwenden:

modelBuilder.Entity<Customer>()
    .HasOne<Customer>()
    .WithOne()
    .HasForeignKey<Customer>(a => a.Id)
    .OnDelete(DeleteBehavior.Restrict);

Begrenzungen

  • Entitätsaufteilung kann nicht für Entitätstypen in Hierarchien verwendet werden.
  • Für jede Zeile in der Haupttabelle muss in jeder der geteilten Tabellen eine Zeile vorhanden sein (die Fragmente sind nicht optional).

Tabellenspezifische Facettenkonfiguration

Einige Zuordnungsmuster führen dazu, dass dieselbe CLR-Eigenschaft einer Spalte in jeder der verschiedenen Tabellen zugeordnet wird. EF7 ermöglicht, dass diese Spalten, unterschiedliche Namen haben. Betrachten Sie beispielsweise eine einfache Vererbungshierarchie:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

Mit der TPT-Vererbungsstrategiewerden diese Typen drei Tabellen zugeordnet. Die Primärschlüsselspalte in jeder Tabelle hat jedoch möglicherweise einen anderen Namen. Beispiel:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 ermöglicht die Konfiguration dieser Zuordnung mithilfe eines verschachtelten Tabellen-Generators:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

Mit der TPC-Vererbungszuordnung kann die Breed-Eigenschaft auch verschiedenen Spaltennamen in verschiedenen Tabellen zugeordnet werden. Sehen Sie sich beispielsweise die folgenden TPC-Tabellen an:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 unterstützt diese Tabellenzuordnung:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });