Mapping avanzato delle tabelle

EF Core offre una notevole flessibilità quando si tratta di eseguire il mapping dei tipi di entità alle tabelle in un database. Ciò diventa ancora più utile quando è necessario usare un database che non è stato creato da Entity Framework.

Le tecniche seguenti sono descritte in termini di tabelle, ma lo stesso risultato può essere ottenuto anche quando si esegue il mapping alle viste.

Suddivisione di tabelle

EF Core consente di eseguire il mapping di due o più entità a una singola riga. Questa operazione è denominata suddivisione delle tabelle o condivisione di tabelle.

Configurazione

Per usare la suddivisione delle tabelle, è necessario eseguire il mapping dei tipi di entità alla stessa tabella, eseguire il mapping delle chiavi primarie alle stesse colonne e almeno una relazione configurata tra la chiave primaria di un tipo di entità e un'altra nella stessa tabella.

Uno scenario comune per la suddivisione delle tabelle consiste nell'usare solo un subset delle colonne della tabella per ottenere prestazioni o incapsulamento maggiori.

In questo esempio Order rappresenta un subset di 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; }
}

Oltre alla configurazione richiesta, viene chiamato Property(o => o.Status).HasColumnName("Status") per eseguire il mapping DetailedOrder.Status alla stessa colonna di 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();
    });

Suggerimento

Per altre informazioni di contesto, vedere il progetto di esempio completo.

Utilizzo

Il salvataggio e l'esecuzione di query sulle entità tramite la suddivisione delle tabelle vengono eseguite nello stesso modo delle altre entità:

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

Entità dipendente facoltativa

Se tutte le colonne usate da un'entità dipendente si trovano NULL nel database, non verrà creata alcuna istanza quando viene eseguita una query. In questo modo è possibile modellare un'entità dipendente facoltativa, in cui la proprietà di relazione nell'entità sarebbe Null. Si noti che ciò si verifica anche se tutte le proprietà del dipendente sono facoltative e impostate su null, che potrebbe non essere previsto.

Tuttavia, il controllo aggiuntivo può influire sulle prestazioni delle query. Inoltre, se il tipo di entità dipendente ha dipendenze proprie, determinare se un'istanza deve essere creata diventa non semplice. Per evitare questi problemi, il tipo di entità dipendente può essere contrassegnato come necessario, vedere Dipendenti uno-a-uno necessari per altre informazioni.

Token di concorrenza

Se uno dei tipi di entità che condividono una tabella ha un token di concorrenza, deve essere incluso anche in tutti gli altri tipi di entità. Questo è necessario per evitare un valore di token di concorrenza non aggiornato quando viene aggiornata solo una delle entità mappate alla stessa tabella.

Per evitare di esporre il token di concorrenza al codice di utilizzo, è possibile crearne uno come proprietà shadow:

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

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

Ereditarietà

È consigliabile leggere la pagina dedicata sull'ereditarietà prima di continuare con questa sezione.

I tipi dipendenti che usano la suddivisione delle tabelle possono avere una gerarchia di ereditarietà, ma esistono alcune limitazioni:

  • Il tipo di entità dipendente non può usare il mapping TPC perché i tipi derivati non sarebbero in grado di eseguire il mapping alla stessa tabella.
  • Il tipo di entità dipendente può usare il mapping TPT, ma solo il tipo di entità radice può usare la suddivisione delle tabelle.
  • Se il tipo di entità principale usa TPC, solo i tipi di entità che non dispongono di discendenti possono usare la suddivisione delle tabelle. In caso contrario, è necessario duplicare le colonne dipendenti nelle tabelle corrispondenti ai tipi derivati, complicando tutte le interazioni.

Suddivisione di entità

EF Core consente di eseguire il mapping di un'entità alle righe in due o più tabelle. Questa operazione è denominata suddivisione di entità.

Configurazione

Si consideri, ad esempio, un database con tre tabelle che contengono i dati dei clienti:

  • Tabella Customers per le informazioni sui clienti
  • Tabella PhoneNumbers per il numero di telefono del cliente
  • Tabella Addresses per l'indirizzo del cliente

Ecco le definizioni per queste tabelle in 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
);

Ognuna di queste tabelle viene in genere mappata al proprio tipo di entità, con relazioni tra i tipi. Tuttavia, se tutte e tre le tabelle vengono sempre usate insieme, può essere più conveniente eseguirne il mapping a un singolo tipo di entità. Ad esempio:

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

Questo risultato viene ottenuto in EF7 chiamando SplitToTable per ogni divisione nel tipo di entità. Ad esempio, il codice seguente suddivide il Customer tipo di entità nelle Customerstabelle , PhoneNumberse Addresses illustrate in precedenza:

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

Si noti anche che, se necessario, è possibile specificare nomi di colonna diversi per ognuna delle tabelle. Per configurare il nome della colonna per la tabella principale, vedere Configurazione di facet specifica della tabella.

Configurazione della chiave esterna di collegamento

Il FK che collega le tabelle mappate ha come destinazione le stesse proprietà in cui è dichiarato. In genere non verrebbe creata nel database, perché sarebbe ridondante. Esiste tuttavia un'eccezione per quando viene eseguito il mapping del tipo di entità a più tabelle. Per modificare i facet, è possibile usare l'API Fluent per la configurazione delle relazioni:

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

Limiti

  • La suddivisione delle entità non può essere usata per i tipi di entità nelle gerarchie.
  • Per qualsiasi riga della tabella principale deve essere presente una riga in ognuna delle tabelle suddivise (i frammenti non sono facoltativi).

Configurazione di facet specifica della tabella

Alcuni modelli di mapping comportano il mapping della stessa proprietà CLR a una colonna in ognuna di più tabelle diverse. EF7 consente a queste colonne di avere nomi diversi. Si consideri, ad esempio, una semplice gerarchia di ereditarietà:

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

Con la strategia di mapping dell'ereditarietà TPT, questi tipi verranno mappati a tre tabelle. Tuttavia, la colonna chiave primaria in ogni tabella può avere un nome diverso. Ad esempio:

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 consente di configurare questo mapping usando un generatore di tabelle annidato:

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

Con il mapping di ereditarietà TPC, la Breed proprietà può anche essere mappata a nomi di colonna diversi in tabelle diverse. Si considerino ad esempio le tabelle TPC seguenti:

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 supporta questo mapping di tabelle:

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