Condividi tramite


Introduzione ad Entity Framework con C#, parte II: Code-First e Migrazioni (it-IT)


Introduzione

In questo secondo articolo, approfondiremo l'utilizzo di Entity Framework in relazione al paradigma Code-First, che - come abbiamo visto in precedenza - non necessita di uno schema database già esistente, permettendo piuttosto di operare a livello dati partendo dalla definizione degli oggetti/classi che caratterizzeranno le entità. Anche in questo caso, verrà fatto uso di C# in ambito WinForms, ma - come detto nel precedente articolo - questa scelta non andrà ad inficiare una diversa destinazione d'utilizzo che lo sviluppatore potrà ritenere opportuna.

Perché Code-First?

Avendo valutato la praticità dell'approccio Database-First, viene da domandarsi quali siano le motivazioni che dovrebbero spingerci verso il paradigma Code-First. D'altra parte, si potrebbe progettare la base dati, elaborando direttamente su SQL Server (ad esempio) il nostro schema, le entità, le chiavi e le relazioni, demandando poi al pratico Database-First la derivazione delle classi e loro interconnessioni, peraltro sfruttando la GUI che i modelli EDMX mettono a disposizione.

In realtà, vi sono diversi motivi per i quali Code-First può rappresentare una scelta appropriata. In primis, si rivela essere la sola strada percorribile nel caso in cui la base dati non esista, ma debba essere creata dall'applicazione. In questo caso, saranno le classi definite dallo sviluppatore a rappresentare il modello su cui creare il database e le sue entità. Code-First si interfaccia in modo ottimale con database già esistenti: inoltre, nel caso la base dati debba essere accessibile solo in parte (ad es.: ci interessa derivare tabelle non complete), Code-First consente la dichiarazione di classi che contengano solo quanto effettivamente ci serve. Abbiamo un maggiore controllo sulla struttura stessa del database, che verrà determinata dal codice scritto. Nel caso sia necessario applicare modifiche alla base dati, disporremo di utili strumenti di migrazione, ridistribuibili con l'applicazione. Inoltre, - ed in parte qui ci ripetiamo - il codice in uso è effettivamente quello scritto, evitando l'over-bloating che l'approccio Database-First, per sua struttura intrinseca, in un certo senso impone, con i suoi numerosi files e la derivazione totale delle tabelle referenziate.

Ricordiamo inoltre che - sebbene il presente articolo si occupi di EF 6.1.3 - dalla versione 7.0 Code-First diventa l'unico paradigma utilizzabile, e quindi conoscerlo fin d'ora comporta un vantaggio non indifferente per il futuro.

Creazione di una base dati

In questa sezione, e relativi punti, vedremo come poter creare un database contenente alcune tabelle, utilizzando le sole classi che andremo a scrivere. Prima di ciò, riepiloghiamo brevemente come referenziare Entity Framework nel nostro progetto, come già fatto nel precedente articolo, «Introduzione ad Entity Framework con C#, parte I (it-IT)», al quale rimandiamo il lettore per le informazioni preliminari necessarie a questa seconda parte.

Referenziare Entity Framework nel progetto

Per referenziare EF all'interno del nostro progetto, ossia renderne le librerie accessibili alla soluzione, è necessario anzitutto creare un nuovo progetto in Visual Studio, scegliendo il template di cui abbiamo necessità (nell'esempio, come anticipato, selezionerò C# su WinForms). Salviamo quindi la soluzione (importante per evitare avvisi in fase di aggiunta di EF), ed apriamo il gestore di pacchetti Nuget.

In esso, cercheremo il package Entity Framework, e semplicemente lo aggiungeremo alla nostra solution mediante pressione del tasto «Installa». Al termine dell'operazione, vedremo come tra le References di progetto saranno state inserite quelle relative ad EF.

Siamo quindi pronti per utilizzare le potenzialità del nostro ORM relativamente allo sviluppo. 

Predisposizione allo sviluppo

Come già visto nel precedente articolo, sarà necessario aggiungere al progetto un'entità dati di tipo ADO.NET. A questo fine, cliccando sulla soluzione, e scegliendo la voce «Add New Item» andremo a scegliere dalla sezione «Data» un oggetto di tipo ADO.NET Entity Data Model, che chiameremo anche in questo caso "TechnetModello".

Il wizard di creazione proseguirà, e questa volta selezioneremo, come modello di contenuti, un modello vuoto predisposto per il Code-First

Alla pressione del tasto «Finish», verrà creata nel progetto una classe di nome TechnetModello, che estende il tipo DbContext. Questo è il tipo fondamentale che ci permetterà di creare un database iniziale (o comunque di istanziare la connessione verso un database esistente), consentendo la definizione di classi/entità che verranno tradotte nelle tabelle utilizzate dal database stesso.

Impostare una stringa di connessione

Aprendo la classe TechnetModello, vedremo il suo costruttore presentarsi nella forma seguente:

public TechnetModello() : base("name=TechnetModello")
{
}

Viene cioè fornita al DbContext sottostante una stringa, nel formato "name=TechnetModello". Tale sintassi si riferisce alla stringa di connessione da utilizzare quanto la classe viene referenziata. Al posto di tale stringa può essere passata, ad esempio, un'intera stringa di connessione ODBC ricavata da file UDL. Se invece la si lascia nel formato "name=...", il nome della stringa va ricercato nel file App.config, in cui troveremo una sezione di nome connectionStrings, al cui interno possono essere differenziate diverse stringhe di connessione, ciascuna avente nome diverso. Nel nostro caso, "name=TechnetModello" dà ad intendere che all'interno di App.config, esiste una stringa di connessione di nome TechnetModello.

Verificando il contenuto del file, constateremo che �� proprio così:

<connectionStrings>
  <add name="TechnetModello" connectionString="data source=(LocalDb)\v11.0;initial catalog=ArticoloEF02.TechnetModello;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>

Su tale stringa è possibile applicare tutte le modifiche del caso: ad esempio, potremmo volerci riferire ad una istanza di SQL Server già esistente, oppure modificare il nome del database creato, che nella connectionstring è definito dalla proprietà Initial Catalog. LocalDb è una versione di SQL Server Express creata specificamente per gli sviluppatori. Viene tipicamente installata con Visual Studio, e dispone di molte caratteristiche delle versioni usuali di SQL Server, permettendo al programmatore di poter evitare l'installazione di altre istanze che vadano ad appesantire le macchine dedicate allo sviluppo. Nel caso in esame, utilizzeremo pertanto tale motore di database, limitandoci a modificare il nome di database, che diventerà TECHNET. La stringa di connessione in App.config assumerà pertanto il seguente valore:

<connectionStrings>
  <add name="TechnetModello" connectionString="data source=(LocalDb)\v11.0;initial catalog=TECHNET;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>

  
A questo punto, sarà necessario creare la prima migrazione, che - come abbiamo precedentemente osservato - permette in contesti Code-First di eseguire l'aggiornamento del modello dati fisico sulla base delle classi predisposte.

Migrazione di startup

Abilitamo per il progetto le migrazioni, eseguendo da Package Manager Console l'istruzione 

Enable-Migrations

E creiamo quindi la migrazione iniziale, mediante il comando

Add-Migration InitialCreate –IgnoreChanges

A questo punto, il nostro progetto sarà arricchito dalla directory Migrations, la quale conterrà un file di configurazione, denominato Configuration.cs, e dal primo file di migrazione, che ora siamo in condizione di poter eseguire.

Procediamo quindi digitando nella Package Manager Console l'istruzione Update-Database, ed attendiamo il termine dell'elaborazione.
Una volta conclusa, saremo informati delle migrazioni eseguite sul database.

Per verificare cosa sia successo lato server, possiamo connetterci all'istanza di LocalDb, ad esempio tramite SQL Management Studio: osserveremo come sia stato creato il database desiderato.

Le operazioni relative alle migrazioni saranno da ripetere in tutti quei casi in cui si necessita di effettuare modifiche alla struttura del database stesso. Proprio perché ragioniamo secondo il paradigma Code-First, sarà il nostro codice a dover effettuare le variazioni sul modello, e non viceversa. Vedremo un secondo esempio di migrazione tra poco, non appena avremo creato qualche entità significativa.

Creare classi di entità

Ora che disponiamo di un database, supponiamo di voler creare in esso due tabelle. Esse corrisponderanno, rispettivamente, ad una entità che ci servirà a memorizzare una banale anagrafica di prodotti, mentre la seconda conterrà alcune informazioni di dettaglio circa le famiglie merceologiche dei prodotti disponibili. Possiamo operare in due modi, ovvero creare un nuovo file di classe, oppure accodare le nostre classi a quanto già contenuto nel file TechnetModello.cs. Per questioni di praticità, opteremo qui per la seconda strada, anche se - per rendere il codice più manutenibile - in applicazioni reali ha perfettamente senso che ciascuna classe abbia il proprio file caratteristico.

Classe Articoli 

Definiamo quindi una classe di nome Articoli. Come abbiamo già visto in precedenza, la classe conterrà proprietà che - su database - corrisponderanno ad altrettanti campi. Supponiamo allora di voler generare una classe estremamente semplice, costituita da un codice di prodotto, una descrizione, ed un codice famiglia. Potremo pertanto scrivere un codice come il seguente:

public class  Articoli
{
    public string  Codice { get; set; }
    public string  Descrizione { get; set; }
    public string  CodFamiglia { get; set; }
}

Abbiamo accennato, nello scorso articolo, alle cosiddette DataAnnotations, ovvero ad istruzioni sintetiche di grande aiuto nel definire particolari proprietà dei nostri campi. Nella sintassi Code-First, esse assumono un'importanza notevole, in quanto - dovendo essere lo sviluppatore a tracciare la struttura delle classi - consentono di ampliare la definizione di proprietà specificando ulteriori carattestiche proprie del campo, come ad esempio la sua lunghezza, l'eventuale obbligatorietà, il suo essere parte della chiave primaria di tabella, e così via.

Impieghiamo quindi le DataAnnotations per delineare meglio il profilo della nostra tabella Articoli.
Nell'esempio seguente, poniamo il vincolo di chiave sul solo campo Codice, impostandone altresì l'obbligatorietà, e - mediante la DataAnnotation StringLength - definendo una lunghezza massima per ciascun campo.

public class  Articoli
{
    [Key]
    [StringLength(15)]
    [Required]
    public string  Codice { get; set; }
    [StringLength(50)]
    public string  Descrizione { get; set; }
    [StringLength(6)]
    public string  CodFamiglia { get; set; }
}

    

Classe Famiglie

La classe Famiglie conterrà invece due soli campi: CodFamiglia, ovviamente in chiave, e Descrizione, rappresentante la descrizione estesa della famiglia.

public class  Famiglie
{
    [Key]
    [StringLength(6)]
    [Required]
    public string  CodFamiglia { get; set; }
 
    [StringLength(35)]
    public string  Descrizione { get; set; }
}

    

Classe TechnetModello

 
Affinché le due classi possano essere sfruttate dal modello, ed associate alla base dati, esse devono essere dichiarate come proprietà di tipo DbSet all'interno della classe TechnetModello. Dovremo pertanto operare la seguente modifica:

public class  TechnetModello : DbContext
{
    public TechnetModello() : base("name=TechnetModello") { }
     
    public virtual  DbSet<Articoli> Articoli { get; set; }
    public virtual  DbSet<Famiglie> Famiglie { get; set; }
}

 

Seconda migrazione

Con il nuovo modello pronto all'utilizzo, possiamo ora procedere alla pubblicazione della seconda migrazione, questa volta avente il compito di creare le tabelle sul database. 
Da Package Manager Console, eseguiamo l'istruzione 

Add-Migration Tabelle_di_base

E successivamente, eseguiamola come fatto precedentemente

Update-Database

Collegandoci quindi all'istanza di LocalDb, vedremo come siano state aggiunte le nostre tabelle, al cui nome classe è stato aggiunto il carattere finale che in inglese designa il plurale.
Questa particolarità, un po' fastidiosa nella nostra lingua, può essere aggirata con una apposita DataAnnotation, che analizzeremo nel contesto della prossima migrazione.

Dati iniziali nel contesto di migrazione

Può accadere che, successivamente al rilascio di aggiornamenti del modello, si desideri fornire dei dati di tabella preimpostati (si pensi, ad esempio, a tabelle ministeriali, che devono essere fornite così come sono, e soprattutto pronte all'uso). Le migrazioni ci consentono di distribuire dati precompilati, sfruttando il metodo Seed() che si trova nel file Configuration.cs. Tale metodo viene eseguito successivamente alle migrazioni, e come tale è adatto ad eseguire diverse operazioni sulla base dati.

Seguendo il nostro esempio, vorremo che la migrazione del nostro applicativo popoli le tabelle Articoli e Famiglie con alcuni dati, che ci serviranno per eseguire dei test.
Il metodo Seed diverrà pertanto:

protected override  void Seed(ArticoloEF02.TechnetModello context)
{
 
    context.Articoli.AddOrUpdate(
        new Articoli { Codice = "Test001", Descrizione = "Articolo di Test 001", CodFamiglia = "F01" },
        new Articoli { Codice = "Test002", Descrizione = "Articolo di Test 002", CodFamiglia = "F01" },
        new Articoli { Codice = "Test003", Descrizione = "Articolo di Test 003", CodFamiglia = "F03" },
        new Articoli { Codice = "Test004", Descrizione = "Articolo di Test 004", CodFamiglia = "F02" },
        new Articoli { Codice = "Test005", Descrizione = "Articolo di Test 005", CodFamiglia = "F02" },
        new Articoli { Codice = "Test006", Descrizione = "Articolo di Test 006", CodFamiglia = "F01" }
        );
 
    context.Famiglie.AddOrUpdate(
        new Famiglie{CodFamiglia = "F01", Descrizione = "Prodotti Finiti"},
        new Famiglie{CodFamiglia = "F02", Descrizione = "Semilavorati"},
        new Famiglie{CodFamiglia = "F03", Descrizione = "Materie Prime"}
        );
 
}

        
Si noti come esso utilizzi, come parametro, una variabile context di tipo TechnetModello: questo ci permetterà quindi di riferirci direttamente alla nostra entità dati, richiamando - sulle classi che rappresentano le tabelle - quei metodi che permettono di modificare i dati sottostanti. Nel nostro caso, aggiungiamo 6 articoli e 3 famiglie.

Possiamo ora utilizzare nuovamente il comando 

Update-Database

Non vi sono migrazioni in sospeso, ma al termine di tale comando sarà comunque sempre eseguito il metodo Seed(). Pertanto, andando nuovamente ad interrogare la nostra base dati, e chiedendo un banale SELECT dalle tabelle create, otterremo il risultato seguente:

Accesso ai dati 

In questa sezione vedremo alcuni semplici esempi per effettuare l'accesso ai dati nei contesti di selezione, inserimento, aggiornamento e cancellazione.
Per tutti questi casi, si deve anzitutto disporre di un contesto dati correttamente inizializzato. Nel nostro caso, ciò è ottenibile con la semplice definizione di una nuova referenza ad una variabile che appartenga alla classe che estende DbContext, ovvero:

TechnetModello db = new  TechnetModello();

Più generalmente, se vogliamo inizializzare un contesto dati e demandare all'ambiente il compito di disporre di tale contesto una volta terminate le operazioni desiderate, potremo utilizzare la clausola using, in questo modo:

using (TechnetModello db = new TechnetModello()) { 
    // TO DO: operazioni di lettura/scrittura
}

Interrogazione dati

Viene qui mostrata una semplice routine per effettuare la lettura ciclica prima, e particolare poi, dei record presenti nella tabella Articoli.
Come risulta essere chiaro dalle premesse, le nostre tabelle possono essere pensate come liste appartenenti ad un dato tipo, in parte precompilate (i dati già presenti in tabella) e modificabili.
Di conseguenza, una volta inizializzato il contesto dati, l'entità Articoli può essere scorsa come di normale consuetudine.

using (TechnetModello db = new TechnetModello()) {
 
    foreach (Articoli a in db.Articoli)
    {
        MessageBox.Show(a.Codice + " " + a.Descrizione);
    }
 
}

Lo snippet di cui sopra inizializza il contesto dati sulla variabile db. Dopodichè, all'interno di un ciclo for/each, andiamo a leggere le singole entità di tipo Articoli contenute nella lista db.Articoli, emettendone a video il codice più descrizione. In questi casi, la sintassi LINQ è ovviamente utilissima per eseguire interrogazioni o selezioni mirate.

Si supponga ad esempio di voler estrarre una variabile di tipo Articoli, rappresentante la prima occorrenza di Articoli che ha codice famiglia pari a F02.
Con la sintassi LINQ, potremo scrivere concisamente il seguente snippet:

using (TechnetModello db = new TechnetModello())
{
    Articoli a = db.Articoli.Where((x) => x.CodFamiglia == "F02").FirstOrDefault();
    if (a != null) MessageBox.Show(a.Codice);
}

Inserimento nuovo record

Le operazioni di inserimento passano - ovviamente - per il referenziamento di una variabile di tipo compatibile con quello della lista, per poi accodarla a quest'ultima, una volta ne si siano valorizzate le proprietà. Una nuova variabile di tipo Articoli può essere dichiarata come di seguito:

Articoli a = new  Articoli() { Codice = "PROVA", Descrizione = "Articolo inserito da codice", CodFamiglia = "" };

Ovviamente, il DbSet Articoli dispone di un metodo Add(), che ci consentirà di passare tale variabile al set per aggiungerlo ad esso. Senza controlli di altro tipo, ci esporremo però ad un rischio, ovvero di poter tentare di inserire chiavi duplicate in archivio. Diventa quindi necessario, prima di inserire la nostra variabile, verificare se i suoi elementi chiave non risultino già specificati.
Con LINQ, il tutto diventa molto semplice:

Articoli a = new  Articoli() { Codice = "PROVA", Descrizione = "Articolo inserito da codice", CodFamiglia = "" };
 
if (db.Articoli.Find(a.Codice) != null)
    MessageBox.Show("Articolo già presente su tabella Articoli");
else
    db.Articoli.Add(a);

    
Definita la nostra variabile di tipo Articoli, andiamo ad eseguire il metodo Find() sul DbSet rappresentante la tabella, cercando in esso eventuali occorrenze di a.Codice, ovvero della chiave primaria di tabella. Nel caso si riscontri un elemento già esistente, emetteremo un avviso di impossibilità a procedere, mentre in caso contrario aggiungeremo l'elemento.

Tuttavia, nelle operazioni di modifica dati non è sufficiente agire sulle entità: è necessario consolidare le modifiche, ovvero informare Entity Framework della volontà di salvare le variazioni occorse durante l'utilizzo dei DbSet. DbContext possiede un metodo di nome SaveChanges(), con il quale appunto eseguire tale consolidamento. Dispone inoltre di una proprietà da utilizzare ai fini del controllo rispetto alle modifiche avvenute sul database. È quindi possibile, prima di eseguire le routine di salvataggio, introdurre un controllo preliminare per sincerarsi che tale procedura sia effettivamente necessaria.

using (TechnetModello db = new TechnetModello())
{
    Articoli a = new  Articoli() { Codice = "PROVA", Descrizione = "Articolo inserito da codice", CodFamiglia = "" };
 
    if (db.Articoli.Find(a.Codice) != null)
        MessageBox.Show("Articolo già presente su tabella Articoli");
    else
        db.Articoli.Add(a); 
 
 
    if (db.ChangeTracker.HasChanges()) db.SaveChanges();  
}

Dal momento che lo snippet di cui sopra potrebbe non produrre variazioni nel DbSet (è il caso in cui venga riscontrata una chiave duplicata), utilizziamo la proprietà ChangeTracker ed il suo metodo HasChanges per verificare se siano avvenute modifiche sul DbContext. In caso affermativo, si esegue il metodo SaveChanges e si consolidano i dati attuali.

Aggiornamento record esistenti

A questo punto, le operazioni di update e delete dei dati diventano ovvie. Supponiamo di voler modificare il record appena inserito, ovvero avente campo chiave Codice = PROVA.
La prima cosa da fare sarà individuarlo sul DbSet Articoli, e successivamente - se esso risulta presente - potremo modificarne le proprietà. Tale operazione aggiornerà al contempo il ChangeTracker, mediante il quale determineremo se eseguire un consolidamento dei dati.

Molto banalmente, il codice sarà:

using (TechnetModello db = new TechnetModello())
{
    Articoli a = db.Articoli.Find("PROVA");
    if (a != null)
    {
        a.CodFamiglia = "F03";
    }
 
    if (db.ChangeTracker.HasChanges()) db.SaveChanges();
}

In questo caso, desideriamo aggiornare, per il singolo record, il codice famiglia, portandolo a F03. Si noti come sia sufficiente l'impostazione della proprietà corrispondente, una volta individuato il record. L'operazione può essere ovviamente eseguita in maniera massiva, su tutti gli elementi desiderati. Il metodo SaveChanges() dovrà essere chiamato una sola volta, in conclusione dell'elaborazione, per eseguire un commit globale delle modifiche.

Eliminazione record 

Anche l'eliminazione di un record è semplice: si tratta di rimuovere un particolare elemento dal DbSet Articoli, e nuovamente consolidarne la variazione.
Nello snippet a seguire, viene eseguita la ricerca dell'articolo avente campo chiave Codice = PROVA, e sua successiva rimozione se riscontrato:

using (TechnetModello db = new TechnetModello())
{
     
    Articoli a = db.Articoli.Find("PROVA");
    if (a != null) db.Articoli.Remove(a);
 
    if (db.ChangeTracker.HasChanges()) db.SaveChanges();
}

Modificare i nomi di tabella 

Come ultimo aspetto di questo articolo, vediamo ancora come modificare i nomi delle tabelle fisiche, in questa sede tramite DataAnnotations.
Ulteriori considerazioni in merito verranno fatte in futuro, quando tratteremo più da vicino le cosiddette Fluent API.

Abbiamo visto come, per impostazione predefinita, Entity Framework generi le tabelle assegnando loro un carattere, in coda al nome, che definisce il plurale in lingua inglese. Dal momento che, in italiano, abbiamo già declinato i nomi classe al plurale, vogliamo evitare che le tabelle vengano create con una nomenclatura differente da quella prevista. Sarà sufficiente anteporre alla dichiarazione delle classi Articoli e Famiglie la DataAnnotation Table, indicando il nome che le tabelle dovranno assumere.

Il file TechnetModello assumerà pertanto il seguente aspetto:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
 
public class  TechnetModello : DbContext
{
    public TechnetModello() : base("name=TechnetModello") { }
     
    public virtual  DbSet<Articoli> Articoli { get; set; }
    public virtual  DbSet<Famiglie> Famiglie { get; set; }
}
 
[Table("Articoli")]
public class  Articoli
{
    [Key]
    [StringLength(15)]
    [Required]
    public string  Codice { get; set; }
    [StringLength(50)]
    public string  Descrizione { get; set; }
    [StringLength(6)]
    public string  CodFamiglia { get; set; }
}
 
[Table("Famiglie")]
public class  Famiglie
{
    [Key]
    [StringLength(6)]
    [Required]
    public string  CodFamiglia { get; set; }
 
    [StringLength(35)]
    public string  Descrizione { get; set; }
}

A questo punto, possiamo generare una nuova migrazione 

Add-Migration NomiTabelle

e successivamente eseguirla 

Update-Database 

Terminata la migrazione, andando ad interrogare il database TECHNET vedremo come il nome delle tabelle sia stato modificato, riflettendo ora quanto desiderato.

Download 

Il codice sorgente utilizzato in questo articolo è liberamente scaricabile all'indirizzo https://code.msdn.microsoft.com/Introduzione-ad-Entity-4ef8cec6

Conclusione

Sono stati qui brevemente delineati le metodologie concernenti il paradigma Code-First di Entity Framework, mostrando alcuni esempi di utilizzo delle entità create, e soffermandosi sulle possibilità di modellazione dati sottostante al contesto. Si consiglia al lettore di prendere confidenza con queste nozioni, unitamente a quelle del precedente articolo, in attesa di proseguire in questa panoramica, con i prossimi appuntamenti della serie. 

Altre lingue

Il presente articolo è disponibile inoltre nelle seguenti localizzazioni:

Articoli in questa serie