Le novità di Entity Framework (Core) 7

di Stefano Mostarda, in LINQ,
Le novità di Entity Framework (Core) 7

Entity Framework 7 è il successore di Entity Framework Core 6. Sebbene questa affermazione possa sembrare scontata, è opportuno farla in quanto si potrebbe essere portati a pensare che Entity Framework 7 sia il successore di Entity Framework 6 e non Entity Framework Core 6.Come tutte le versioni precedenti, questa versione non introduce grandissime novità, ma tanti piccoli miglioramenti in diversi settori. Ne sono un esempio i miglioramenti apportati alle performance, alla gestione dell'ereditarietà, all'interception, l'introduzione delle colonne JSON, dei template T4 e delle bulk update e delete.Vediamo ora queste novità in dettaglio.

Colonne JSON

Entity Framework Core (EF d'ora in poi) ha sempre lavorato sia con database relazionali, sia con database No Sql come CosmosDB. Molti database relazionali ormai da anni offrono funzionalità parzialmente assimilabili a quelle di database No SQL. Un esempio di queste funzionalità è la capacità di memorizzare nelle colonne dati in formato JSON e di effettuare su esse anche query e modifiche.

A partire dalla versione 7, EF supporta nativamente la scrittura che la lettura di colonne JSON e, soprattutto, le operazioni di filtro e ordinamento su campi dell'oggetto memorizzato in una colonna JSON. Prendiamo come esempio il seguente modello dove una persona ha un id, un name e informazioni di contatto all'interno delle quali c'è un indirizzo e una lista di email e telefoni.

public class Person
{
  public int Id { get; set; }
  public string FullName { get; set; }
  public ContactInfo Contact { get; set; }
}

public class ContactInfo
{
  public List<ContactDetailInfo> Emails { get; set; }
  public List<ContactDetailInfo> Phones { get; set; }
  public Address Address { get; set; }
}

public class ContactDetailInfo
{
  public string Value { get; set; }
}

public class Address
{
  public string Street { get; set; }
  public string City { get; set; }
  public string Postcode { get; set; }
  public string Country { get; set; }
}

Il modello mostrato nell'esempio è molto semplice da mappare nel modello relazionale. I campi dell'indirizzo nel contatto vengono messi nella tabella della People mentre per le email e i telefoni vengono create sotto tabelle con foreign key verso la persona. Tuttavia, con EF 7 possiamo memorizzare l'intero oggetto ContactInfo in una colonna JSON evitando così di avere sotto tabelle. Grazie alle API di mapping, la presenza della colonna JSON è trasparente al codice e quindi possiamo lavorare ad oggetti esattamente come se i dati fossero mappati col modello relazionale. Tuttavia, la scelta di utilizzare colonne JSON ha implicazioni non banali e, come tutte le cose, va valutato il rapporto costi/benefici prima dell'utilizzo.

L'ovvio vantaggio dell'utilizzo di colonne JSON è quello di non avere tabelle collegate e quindi si evitano molte JOIN quando si devono recuperare dati da queste. Un altro vantaggio è che le operazioni di scrittura non riguardano più tabelle, ma solo la tabella principale in quanto tutti i dati collegati sono li.

Uno degli svantaggi delle colonne JSON consiste nelle performance in fase di ricerca per uno dei valori dell'oggetto memorizzato nella colonna. Se prendiamo come esempio Sql Server come motore di database, il provider di EF crea la colonna come nvarchar(max) e quindi non è possibile fare un'indicizzazione su un campo dell'oggetto che ci mettiamo dentro. Facendo un esempio pratico, se vogliamo recuperare tutte le persone di una determinata nazione, non abbiamo modo di indicizzare il campo Country. Il risultato è che per effettuare la ricerca, il database deve effettuare il table scan della tabella e non può usare indici. Per ovviare al problema possiamo creare nella tabella People una colonna Country, indicizzare quella e usarla per filtrare, ma questo complica sia il mapping che il codice. Un altro svantaggio delle colonne JSON consiste nel fatto che EF non supporta i filtri su un campo JSON di tipo lista. Nel nostro caso, non è possibile recuperare le persone che hanno più di una mail perché questo comporterebbe un filtro sul campo Emails e sebbene il codice compili, a run time otterremmo un'eccezione.

Vediamo ora come mappare il modello verso il database usando una colonna JSON per i dati di contatto.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Person>(entity =>
  {
    entity.OwnsOne(c => c.Contact, b =>
    {
      b.ToJson();
      b.OwnsMany(m => m.Emails);
      b.OwnsMany(m => m.Phones);
      b.OwnsOne(m => m.Address);
    });
  });
}

La navigation property Contact viene mappata come owned entity e tramite il secondo overload del metodo OwnsOne si specifica che deve essere mappata come colonna JSON tramite il metodo ToJson. Successivamente specifichiamo che le proprietà dell'oggetto ContactInfo sono a loro volta owned (non occorre specificare che anche queste sono JSON in quanto lo abbiamo specificato per l'oggetto padre).

Se andiamo a generare il database su Sql Server, otterremo il seguente codice SQL.

CREATE TABLE [People] (
  [Id] int NOT NULL IDENTITY,
  [FullName] nvarchar(max) NOT NULL,
  [Contact] nvarchar(max) NOT NULL,
  CONSTRAINT [PK_People] PRIMARY KEY ([Id])
);

Come detto in precedenza, quando andiamo a lavorare nel codice di persistenza, non ci accorgiamo che una entity abbia colonne JSON o meno. Se vogliamo inserire una nuova persona possiamo usare il seguente codice.

using var ctx = new MyContext();
ctx.Add(new Person
{
  FullName = "Stefano Mostarda",
  Contact = new ContactInfo
  {
    Address = new Address 
    { 
      City = "ROME", 
      Country = "IT",
      Postcode = "00100",
      Street = "Via del corso 1" 
    },
    Emails = new List<ContactDetailInfo> 
    { 
      new() { Value = "test@test.it" }, 
      new() { Value = "test1@test.it" } 
    },
    Phones = new List<ContactDetailInfo> 
    { 
      new() { Value = "063523535" }, 
      new() { Value = "06693475845" } 
    },
  }
});
ctx.SaveChanges();

Al momento di persistere l'entity, sarà EF a recuperare il mapping, serializzare l'oggetto ContactInfo in JSON e creare il codice SQL necessario.

Anche in caso di modifica, le colonne JSON sono trasparenti per il nostro codice. Tuttavia vale la pena capire come agisce il meccanismo di aggiornamento della colonna JSON. Se andiamo ad aggiornare un singolo campo dell'oggetto serializzato nella colonna JSON, EF aggiorna solo quel campo. Se invece modifichiamo più campi, EF aggiorna il primo padre in comune.

Prendiamo come esempio il seguente codice che aggiorna un solo campo.

//C#
using var ctx = new MyContext();
var person = ctx.People.First();
person.Contact.Address.Postcode = "newpostcode";
ctx.SaveChanges();
//SQL Generato
UPDATE [People] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Postcode', JSON_VALUE(@p0, '$[0]'))
WHERE [Id] = @p1;

Come si vede, EF va a modificare solo il campo Postcode col nuovo valore. Vediamo ora cosa succede quando si modificano più campi.

//C#
using var ctx = new MyContext();
var person = ctx.People.First();
person1.Contact.Address.City = "newcity";
person1.Contact.Address.Street = "new street adderess";
ctx.SaveChanges();
//SQL
UPDATE [People] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
WHERE [Id] = @p1;
[@p0='{"City":"newcity","Country":"IT","Postcode":"00100","Street":"new street adderess"}' (Nullable = false) (Size = 83), @p1='1'], CommandType='Text', CommandTimeout='30']

Poiché il campo da aggiornare a livello di database è solo uno (Contact) e che la funzione JSON_MODIFY può aggiornare un solo campo, EF core identifica il primo padre in comune tra i campi da modificare (Address in questo caso) e modifica tutto il campo padre includendo anche i campi non modificati. Seguendo lo stesso meccanismo, se modificassimo la città nell'indirizzo e aggiungessimo una email o un telefono, verrebbe aggiornato l'intero campo Contact in quanto la root sarebbe il primo padre in comune tra tutti gli oggetti coinvolti.

Quando vogliamo effettuare un filtro su una colonna JSON, il codice non cambia rispetto a un filtro su una normale colonna. In questo esempio filtriamo le persone in UK.

var ukPeople = ctx.People.Where(c => c.Contact.Address.Country == "UK").ToList();

Come detto, su Sql Server (e non solo), questo modo di effettuare query è molto lento in quanto non si possono usare indici. In questi casi, il trucco, consigliato dallo stesso team di Sql Server, consiste nel creare una colonna aggiuntiva con il valore da indicizzare. Per non sporcare la nostra entity, possiamo creare una shadow property in fase di mapping, metterci un indice e poi usare questa in fase di query come mostrato nel seguente codice.

//Mapping
modelBuilder.Entity<Person>(entity =>
{
  entity
    .Property<string>("ComputedAddressCountry")
    .HasComputedColumnSql("JSON_VALUE(Contact, '$.Address.Country')");
  entity
    .HasIndex("ComputedAddressCountry");
    &#8230;
}
//Query
ctx.People.Where(c => EF.Property<string>(c, "ComputedAddressCountry") == "UK").ToList();

Così come per i filtri, la stessa tecnica si può applicare a operazioni di raggruppamento, ordinamento e altro ancora. Quando i campi su cui effettuare queste operazioni aumentano, va valutato se non valga la pena usare direttamente il modello relazionale abbandonando le colonne JSON.

Come detto in precedenza, EF ha una limitazione sui campi di tipo lista quindi una query del genere che estrae le persone con più di una email fallisce a run time.

ctx.People.Where(c => c.Contact.Emails.Count > 1).ToList(); //KO

Per chiudere il discorso sulle query, anche le projection sono perfettamente supportate senza alcuna differenza rispetto al mapping verso il modello relazionale. Sarà compito di EF utilizzare la funzione JSON_VALUE per estrarre solo i campi necessari.

Le colonne JSON sono sicuramente la novità principale di EF 7, ma rappresentano un punto di partenza e non un prodotto finito. Ci sono diversi aspetti che possono essere migliorati e nuove funzionalità che possono essere supportate come il supporto a Json path per i filtri sulle liste e un supporto migliore con Sql Server.

4 pagine in totale: 1 2 3 4
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