Rozšířené mapování tabulek

EF Core nabízí spoustu flexibility, pokud jde o mapování typů entit na tabulky v databázi. To se stává ještě užitečnější v případě, že potřebujete použít databázi, kterou ef nevytvořil.

Následující techniky jsou popsány z hlediska tabulek, ale stejný výsledek lze dosáhnout i při mapování na zobrazení.

Dělení tabulky

EF Core umožňuje namapovat dvě nebo více entit na jeden řádek. Tomu se říká rozdělení tabulky nebo sdílení tabulek.

Konfigurace

Pokud chcete použít rozdělení tabulek, musí být typy entit namapované na stejnou tabulku, musí být primární klíče namapované na stejné sloupce a alespoň jedna relace nakonfigurovaná mezi primárním klíčem jednoho typu entity a druhou ve stejné tabulce.

Běžným scénářem rozdělení tabulky je použití pouze podmnožina sloupců v tabulce pro vyšší výkon nebo zapouzdření.

V tomto příkladu Order představuje podmnožinu .DetailedOrder

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; }
}

Kromě požadované konfigurace voláme Property(o => o.Status).HasColumnName("Status") mapování DetailedOrder.Status na stejný sloupec jako Order.Status.

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();
    });

Tip

Další kontext najdete v úplném ukázkovém projektu .

Využití

Ukládání a dotazování entit pomocí rozdělení tabulky se provádí stejným způsobem jako jiné entity:

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}");
}

Volitelná závislá entita

Pokud jsou NULL všechny sloupce používané závislá entitou v databázi, při dotazování se pro ni nevytvořila žádná instance. To umožňuje modelování volitelné závislé entity, kde vlastnost relace objektu zabezpečení by byla null. Všimněte si, že k tomu dojde také v případě, že všechny vlastnosti závislého objektu jsou volitelné a nastavené na nullhodnotu , která nemusí být očekáváná.

Další kontrola ale může mít vliv na výkon dotazů. Kromě toho, pokud závislý typ entity má závislé vlastní, pak určení, zda by se instance měla vytvořit, stane netiviální. Pokud se chcete těmto problémům vyhnout, může být závislý typ entity označený jako povinný, další informace najdete v tématu Povinné závislosti 1:1.

Tokeny souběžnosti

Pokud některý z typů entit sdílející tabulku má token souběžnosti, musí být zahrnutý i ve všech ostatních typech entit. To je nezbytné, aby se zabránilo zastaralé hodnotě tokenu souběžnosti, pokud se aktualizuje pouze jedna z entit mapovaných na stejnou tabulku.

Aby se zabránilo zveřejnění tokenu souběžnosti pro spotřebující kód, je možné, že ho vytvoříte jako stínovou vlastnost:

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

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

Dědičnost

Než budete pokračovat v této části, doporučujeme přečíst si vyhrazenou stránku o dědičnosti .

Závislé typy používající rozdělení tabulky můžou mít hierarchii dědičnosti, ale existují určitá omezení:

  • Závislý typ entity nemůže použít mapování TPC, protože odvozené typy by nemohly mapovat na stejnou tabulku.
  • Závislý typ entity může použít mapování TPT, ale pouze typ kořenové entity může použít rozdělení tabulky.
  • Pokud typ entity objektu zabezpečení používá TPC, můžou rozdělení tabulky použít pouze typy entit, které nemají žádné potomky. Jinak by závislé sloupce bylo potřeba duplikovat v tabulkách odpovídajících odvozeným typům a komplikovat všechny interakce.

Dělení entity

EF Core umožňuje namapovat entitu na řádky ve dvou nebo více tabulkách. Tomu se říká dělení entit.

Konfigurace

Představte si například databázi se třemi tabulkami, které obsahují zákaznická data:

  • Tabulka Customers s informacemi o zákazníci
  • Tabulka PhoneNumbers telefonního čísla zákazníka
  • Tabulka Addresses adresy zákazníka

Tady jsou definice pro tyto tabulky v SQL Serveru:

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
);

Každá z těchto tabulek by se obvykle mapovala na vlastní typ entity s relacemi mezi těmito typy. Pokud se ale všechny tři tabulky vždy používají společně, může být vhodnější je namapovat všechny na jeden typ entity. Příklad:

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; }
}

Toho dosáhnete v EF7 voláním SplitToTable pro každé rozdělení v typu entity. Například následující kód rozdělí Customer typ entity na Customers, PhoneNumbersa Addresses tabulky uvedené výše:

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);
                });
    });

Všimněte si také, že v případě potřeby je možné pro každou z tabulek zadat různé názvy sloupců. Pokud chcete nakonfigurovat název sloupce pro hlavní tabulku, přečtěte si informace o konfiguraci omezující vlastnosti specifické pro tabulku.

Konfigurace propojování cizího klíče

FK propojující mapované tabulky cílí na stejné vlastnosti, na kterých je deklarována. Normálně by se nevytvořila v databázi, protože by byla redundantní. Existuje ale výjimka, kdy je typ entity namapován na více než jednu tabulku. Pokud chcete změnit omezující vlastnosti, můžete použít rozhraní Fluent API pro konfiguraci relací:

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

Omezení

  • Rozdělení entit se nedá použít pro typy entit v hierarchiích.
  • Pro každý řádek v hlavní tabulce musí být v každé rozdělené tabulce řádek (fragmenty nejsou volitelné).

Konfigurace omezující vlastnosti specifické pro tabulku

Některé vzory mapování mají za následek, že se stejná vlastnost CLR mapuje na sloupec v každé z několika různých tabulek. EF7 umožňuje těmto sloupcům mít různé názvy. Představte si například jednoduchou hierarchii dědičnosti:

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; }
}

U strategie mapování dědičnosti TPT se tyto typy mapují na tři tabulky. Sloupec primárního klíče v každé tabulce ale může mít jiný název. Příklad:

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 umožňuje konfiguraci tohoto mapování pomocí tvůrce vnořených tabulek:

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"));

Pomocí mapování Breed dědičnosti TPC lze vlastnost také mapovat na různé názvy sloupců v různých tabulkách. Představte si například následující tabulky TPC:

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 podporuje toto mapování tabulek:

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");
        });