Le novità di Entity Framework Core 5

di Stefano Mostarda, in Entity Framework Core 5,

Entity Framework Core 5 (EF Core d'ora in poi) rappresenta un deciso passo in avanti nel processo di maturazione di questo framework. Questa versione ha infatti raggiunto la cosiddetta feature-parity con il fratello maggiore EF 6. Infatti, seppur con tecniche diverse, tutte le funzionalità di EF 6 sono adesso anche in EF Core 5. Questo rende più facile la migrazione di un'applicazione basata su EF 6 a EF Core 5.

Le funzionalità principali per allinearsi a EF 6 sono principalmente il supporto alle associazioni many-to-many senza l'utilizzo della join entity, il logging semplificato, il miglioramento dei messaggi di errore nelle query e il supporto al modello Table Per Type (TPT) per il mapping dell'ereditarietà. Oltre a queste, sono state aggiunte moltissime altre funzionalità come la possibilità di cambiare la connessione o la stringa di connessione dopo l'inizializzazione del context, l'aggiunta di attributi per il mapping, la possibilità di filtrare le Include, miglioramenti alle migration e allo scaffolding e molto altro ancora. Cominciamo col parlare della novità più attesa: il many-to-many.

Many-to-many

EF Core ha sempre supportato il many-to-many sfruttando la join entity. EF Core 5 elimina la necessità della join entity permettendoci di modellare le relazioni tra entity in modo object oriented. Prendiamo come esempio l'associazione tra prodotti e tag dove un prodotto può avere più tag associati e un tag può essere associato a più prodotti. Dal punto di vista del database, vengono create le tabelle Product, Tag e ProductTag che contiene l'id del prodotto e l'id del tag. Dal punto di vista delle entity, nelle versioni precedenti di EF Core si devono creare le classi Product, Tag e ProductTag, mentre con EF Core 5 possiamo creare solo le classi Product e Tag dove product contiene una lista di Tag e Tag contiene una lista di Product.

public partial class Product
{
  public Product()
  {
    Tags = new HashSet<Tag>();
  }

  public int Id { get; set; }
  public string Name { get; set; }

  public virtual ICollection<Tag> Tags { get; set; }
}

public partial class Tag
{
  public Tag()
  {
    Products = new HashSet<Product>();
  }

  public int Id { get; set; }
  public string Name { get; set; }

  public virtual ICollection<Product> Products { get; set; }
} 

Una volta create le classi, dobbiamo aggiungere gli entity set al contesto.

public DbSet<Product> Products { get; set; }
public DbSet<Tag> Tags { get; set; } 

A questo punto, EF Core è in grado di individuare le relazioni e il relativo mapping sfruttando le convenzioni e noi dobbiamo solo preoccuparci di scrivere codice per le operazioni di lettura e scrittura. Per inserire prodotti e tag, non ci sono novità particolari, ma per inserire un'associazione, dobbiamo aggiungerla alla collection di una o dell'altra entity. Ad esempio, possiamo inserire un oggetto Tag alla proprietà Tags di Product. Se vogliamo invece rimuovere un'associazione, dobbiamo recuperare il prodotto e i suoi tag e rimuovere il tag dall'associazione. Tutto questo è mostrato nel prossimo esempio.

//inserisce prodotti e tag
ctx.Products.Add(new Product() { Name = "Brooks Glycerine" });
ctx.Products.Add(new Product() { Name = "Brooks Transcend" });
ctx.Tags.Add(new Tag() { Name = "Scarpe neutre" });
ctx.Products.Add(new Tag() { Name = "Scarpe antipronanti" });
ctx.SaveChanges();

//inserisce un'associazione
var p = ctx.Products.Find(1);
var t = ctx.Tags.Find(1);
p.Tags.Add(t);
ctx.SaveChanges();

//elimina un'associazione
var p = ctx.Products.Include(p => p.Tags).First(p => p.Id == 1);
var t = ctx.Tags.Find(1);
p.Tags.Remove(t);
ctx.SaveChanges(); 

Questo codice assomiglia molto a quello usato in EF 6 il che ci permette di migrare codice da EF 6 a EF Core 5 in modo molto semplice.

Tuttavia non ci si è fermati qui e il team ha deciso di andare oltre. Sempre riprendendo l'esempio precedente, cosa succede se ci viene chiesto di salvare anche l'utente e la data di associazione del tag al prodotto? In questo caso, la join entity che finora non abbiamo usato assume dignità di entity e dovrebbe essere mappata come relazione uno a molti verso Product e Tag.

Tuttavia, questo ci costringerebbe a modificare pesantemente il codice già scritto. Per evitare di dover riscrivere molto codice, possiamo mantenere entrambe le relazioni e in fase di query lasciare la relazione originale, mentre in scrittura modificare il codice usando la nuova relazione. Cominciamo con il vedere le entity.

public partial class Product
{
  public Product()
  {
    ProductTags = new HashSet<ProductTag>();
    Tags = new HashSet<Tag>();
  }

  public int Id { get; set; }
  public string Name { get; set; }

  public virtual ICollection<Tag> Tags { get; set; }
  public virtual ICollection<ProductTag> ProductTags { get; set; }
}

public partial class Tag
{
  public Tag()
  {
    ProductTags = new HashSet<ProductTag>();
    Products = new HashSet<Product>();
  }

  public int Id { get; set; }
  public string Name { get; set; }

  public virtual ICollection<Product> Products { get; set; }
  public virtual ICollection<ProductTag> ProductTags { get; set; }
} 

public partial class ProductTag
{
  public int TagId { get; set; }
  public int ProductId { get; set; }
  public Tag Tag { get; set; }
  public Product Product { get; set; }

  public DateTime AssociationDate { get; set; }
  public string User { get; set; }
}

ProductTag rappresenta l'entity di mezzo e contiene i riferimenti a Product e Tag, che a loro volta hanno mantenuto il riferimento reciproco aggiungendo quello a ProductTag.

Con questa nuova versione delle entity, EF Core 5 non è più in grado di capire le relazioni tra oggetti sfruttando le convenzioni, quindi dobbiamo andare a specificare nel context il mapping così come nel seguente esempio.

modelBuilder.Entity<Tag>(entity =>
  entity
    .HasMany(e => e.Products)
    .WithMany(e => e.Tags)
    .UsingEntity<ProductTag>(
      j => j
        .HasOne(pt => pt.Product)
        .WithMany(pt => pt.ProductTags)
        .HasForeignKey(pt => pt.ProductId),
      j => j
        .HasOne(pt => pt.Tag)
        .WithMany(pt => pt.ProductTags)
        .HasForeignKey(pt => pt.TagId),
      j =>
      {
        j.Property(pt => pt.AssociationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
        j.Property(pt => pt.User).IsRequired();
      }
)); 

In questo esempio specifichiamo che l'entity Tag ha più Product (metodo HasMany) che a sua volta ha più Tag (metodo WithMany) e che le due entity sono collegate da una join entity ProductTag tramite il metodo UsingEntity. Questo metodo prende in input una prima lambda che specifica il mapping verso Product, poi una seconda lambda che specifica il mapping verso Tag e poi una terza che specifica il mapping verso la tabella del database.

Per concludere, dobbiamo aggiungere al context un nuovo DbSet che espone la nuova entity.

A questo punto siamo pronti per creare l'associazione tra Product e Tag usando la nuova entity. Abbiamo a disposizione due modi. Il primo consiste nel recuperare un'istanza di Product o Tag e usare la proprietà ProductTags aggiungendo un oggetto di tipo ProductTag. Il secondo consiste nell'istanziare un oggetto ProductTag e aggiungerlo direttamente al DbSet creato nel context.

// Metodo 1
var p = ctx.Products.Find(1);
p.ProductTags.Add(new ProductTag { TagId = 1, User = "stefano" });

//Metodo 2
ctx.ProductTags.Add(new ProductTag { TagId = 1, ProductId = 1, User = "stefano" });

Come si vede dal codice, la join entity offre estrema semplicità quando si tratta di inserire o rimuovere l'associazione grazie al fatto di poter lavorare direttamente su un DbSet.

Ora cambiamo argomento e parliamo di un'altra funzionalità che mancava da EF 6: il mapping TPT

Ereditarietà con il Table Per Type

Fin dalla prima versione, EF Core ha supportato il mapping dell'ereditarietà sfruttando il modello TPH dove l'intera gerarchia di classi viene mappata su una tabella. EF Core 5 aggiunge un nuovo modello dove ogni entity della gerarchia viene mappata su una tabella diversa. In questo modo, ogni tabella contiene solo le colonne che mappano sull'entity associata e hanno una primary key che ha lo stesso valore della primary key della tabella che mappa l'entity base della gerarchia così da associare i record tra loro.

Come esempio prendiamo l'entity base Device dalla quale derivano Laptop e Desktop e vediamo come vengono mappate nel context.

public abstract class Device
{
  public int Id { get; set; }
  public int Name { get; set; }
}

public class Laptop : Device
{
  public int BatteryLifetime { get; set; }
}

public class Desktop : Device
{
  public int CaseHeight { get; set; }
  public int CaseWidth { get; set; }
  public int CaseDepth { get; set; }
} 

//Mapping
modelBuilder.Entity<Device>().ToTable("Device");
modelBuilder.Entity<Laptop>().ToTable("Laptop");
modelBuilder.Entity<Desktop>().ToTable("Desktop");

In questo esempio, il codice rilevante è quello di mapping che mostra come usare il TPT chiamando il metodo ToTable per ogni entity della gerarchia.

Dal punto di vista del database, il mapping TPT offre una maggior pulizia rispetto al TPH soprattutto per il fatto che non obbliga a creare una tabella con molte colonne nullabili. Tuttavia ci sono aspetti legati alle performance che vanno valutati. Prendiamo come esempio una gerarchia con 10 classi. In questo caso avremo anche 10 tabelle e se vogliamo recuperare tutti gli oggetti nel database, dovremo fare una query che implica una join con tutte le tabelle. Se invece cerchiamo solo un oggetto di un certo tipo, avremo comunque una join tra la tabella base e la tabella relativa al tipo cercato. Queste osservazioni non vogliono spingere a non usare il TPT, ma vogliono semplicemente ribadire che l'utilizzo del TPT deve essere valutato molto attentamente rispetto al TPH che rimane consigliato nella maggior parte dei casi.

2 pagine in totale: 1 2
Contenuti dell'articolo

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

Top Ten Articoli

Articoli via e-mail

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

In primo piano

I più letti di oggi

In evidenza

Misc