Zaawansowane mapowanie tabeli

Platforma EF Core oferuje dużą elastyczność, jeśli chodzi o mapowanie typów jednostek na tabele w bazie danych. Staje się to jeszcze bardziej przydatne, gdy trzeba użyć bazy danych, która nie została utworzona przez platformę EF.

Poniższe techniki opisano w kategoriach tabel, ale ten sam wynik można osiągnąć również podczas mapowania na widoki.

Dzielenie tabeli

Program EF Core umożliwia mapowanie co najmniej dwóch jednostek na jeden wiersz. Jest to nazywane dzieleniem tabel lub udostępnianiem tabel.

Konfigurowanie

Aby można było używać dzielenia tabel, typy jednostek muszą być mapowane na tę samą tabelę, należy zamapować klucze podstawowe na te same kolumny i co najmniej jedną relację skonfigurowaną między kluczem podstawowym jednego typu jednostki a drugą w tej samej tabeli.

Typowym scenariuszem dzielenia tabel jest użycie tylko podzestawu kolumn w tabeli w celu zwiększenia wydajności lub hermetyzacji.

W tym przykładzie Order reprezentuje podzbiór .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; }
}

Oprócz wymaganej konfiguracji wywołujemy metodę Property(o => o.Status).HasColumnName("Status") mapowania DetailedOrder.Status na tę samą kolumnę co 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();
    });

Napiwek

Zobacz pełny przykładowy projekt, aby uzyskać więcej kontekstu.

Użycie

Zapisywanie i wykonywanie zapytań dotyczących jednostek przy użyciu dzielenia tabeli odbywa się w taki sam sposób jak inne jednostki:

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

Opcjonalna jednostka zależna

Jeśli wszystkie kolumny używane przez jednostkę zależną znajdują NULL się w bazie danych, żadne wystąpienie nie zostanie utworzone podczas wykonywania zapytań. Umożliwia to modelowanie opcjonalnej jednostki zależnej, w której właściwość relacji dla podmiotu zabezpieczeń będzie mieć wartość null. Należy pamiętać, że może się to zdarzyć również wtedy, gdy wszystkie właściwości zależne są opcjonalne i ustawione na null, co może nie być oczekiwane.

Jednak dodatkowa kontrola może mieć wpływ na wydajność zapytań. Ponadto jeśli typ jednostki zależnej ma własne zależności, określenie, czy wystąpienie powinno zostać utworzone, staje się nietrywialne. Aby uniknąć tych problemów, typ jednostki zależnej może być oznaczony jako wymagany, zobacz Wymagane zależności jeden do jednego, aby uzyskać więcej informacji.

Tokeny współbieżności

Jeśli którykolwiek z typów jednostek współużytkowania tabeli ma token współbieżności, musi być również uwzględniony we wszystkich innych typach jednostek. Jest to konieczne, aby uniknąć nieaktualnej wartości tokenu współbieżności, gdy tylko jedna z jednostek zamapowanych na tę samą tabelę jest aktualizowana.

Aby uniknąć uwidaczniania tokenu współbieżności do kodu zużywającego , możliwe jest utworzenie go jako właściwości w tle:

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

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

Dziedziczenie

Zaleca się przeczytanie dedykowanej strony dotyczącej dziedziczenia przed kontynuowaniem tej sekcji.

Typy zależne korzystające z dzielenia tabel mogą mieć hierarchię dziedziczenia, ale istnieją pewne ograniczenia:

  • Typ jednostki zależnej nie może używać mapowania TPC, ponieważ typy pochodne nie będą mogły mapować na tę samą tabelę.
  • Typ jednostki zależnej może używać mapowania TPT, ale tylko typ jednostki głównej może używać dzielenia tabel.
  • Jeśli typ jednostki głównej używa TPC, tylko typy jednostek, które nie mają żadnych elementów podrzędnych, mogą używać dzielenia tabel. W przeciwnym razie kolumny zależne muszą być zduplikowane w tabelach odpowiadających typom pochodnym, komplikując wszystkie interakcje.

Dzielenie jednostki

Program EF Core umożliwia mapowania jednostki na wiersze w co najmniej dwóch tabelach. Jest to nazywane dzieleniem jednostek.

Konfigurowanie

Rozważmy na przykład bazę danych z trzema tabelami, które przechowują dane klienta:

  • Tabela Customers informacji o klientach
  • Tabela PhoneNumbers numeru telefonu klienta
  • Tabela Addresses adresu klienta

Poniżej przedstawiono definicje tych tabel w programie 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
);

Każda z tych tabel jest zwykle mapowana na własny typ jednostki z relacjami między typami. Jeśli jednak wszystkie trzy tabele są zawsze używane razem, bardziej wygodne może być mapowania ich wszystkich na pojedynczy typ jednostki. Przykład:

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

Jest to osiągane w programie EF7 przez wywołanie SplitToTable każdego podziału w typie jednostki. Na przykład poniższy kod dzieli Customer typ jednostki na Customerstabele , PhoneNumbersi Addresses pokazane powyżej:

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

Należy również zauważyć, że w razie potrzeby można określić różne nazwy kolumn dla każdej z tabel. Aby skonfigurować nazwę kolumny dla tabeli głównej, zobacz Konfiguracja aspektów specyficznych dla tabeli.

Konfigurowanie łączenia klucza obcego

Klucz FK łączący zamapowane tabele jest przeznaczony dla tych samych właściwości, na których jest zadeklarowany. Zwykle nie zostanie ona utworzona w bazie danych, ponieważ byłaby ona nadmiarowa. Istnieje jednak wyjątek, gdy typ jednostki jest mapowany na więcej niż jedną tabelę. Aby zmienić jego aspekty, możesz użyć konfiguracji relacji Fluent API:

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

Ograniczenia

  • Dzielenie jednostek nie może być używane dla typów jednostek w hierarchiach.
  • W przypadku dowolnego wiersza w tabeli głównej musi istnieć wiersz w każdej z podzielonych tabel (fragmenty nie są opcjonalne).

Konfiguracja aspektów specyficznych dla tabeli

Niektóre wzorce mapowania powodują mapowanie tej samej właściwości CLR na kolumnę w każdej z wielu różnych tabel. Program EF7 umożliwia tym kolumnom różne nazwy. Rozważmy na przykład prostą hierarchię dziedziczenia:

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

Dzięki strategii mapowania dziedziczenia TPT te typy zostaną zamapowane na trzy tabele. Jednak kolumna klucza podstawowego w każdej tabeli może mieć inną nazwę. Przykład:

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

Program EF7 umożliwia skonfigurowanie tego mapowania przy użyciu konstruktora zagnieżdżonych tabel:

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

Za pomocą mapowania Breed dziedziczenia TPC właściwość może być również mapowana na różne nazwy kolumn w różnych tabelach. Rozważmy na przykład następujące tabele 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])
);

Program EF7 obsługuje to mapowanie tabeli:

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