TECNICHE PER LAVORARE CON I DATI
CREARE LA PRIMA MIGRATIONS CON EFCORE
Quando ci chiedono di implementare una nuova funzionalità, capita sovente la necessità di aggiungere nuove colonne al database. Abbiamo visto che esistono due approcci, il primo Database-First consiste nel collegarsi al database ed eseguire gli script necessari per aggiungere una nuova colonna in una tabella. Utilizzando l’approccio Code-First prima si modifica il codice dell’applicazione poi con il meccanismo delle Migrations di EFCore si apportano le modifiche al database.
Vediamo come creare la prima Migrations.
Sotto la cartella Migrations puoi vedere il codice prodotto da questo comando. È una normalissima classe C# che deriva da Migrations e che ha due metodi molto importanti. Up e Down. Up viene invocata quando dobbiamo evolvere la struttura del database, mentre Down quando dobbiamo far tornare il database prima che la Migrations fosse applicata.
APPLICARE LE MIGRATIONS AL DATABASE
Il comando che hai visto serve per aggiornare il modello concettuale, quando dobbiamo applicare la Migrations prodotta dal comando precedente ed evolvere la struttura del database usiamo questo comando: dotnet ef database update.
Quando vogliamo che venga applicato il metodo Up per evolvere il database e modificarne la sua struttura utilizziamo il comando sopra riportato. Possiamo anche far devolvere il database con lo stesso comando, tutto dipende dal nome delle Migrations che impostiamo. Nel nostro caso non abbiamo una Migrations precedente a cui tornare, per cui se vogliamo riportare indietro il database dobbiamo indicare 0 come nome della Migrations.
Lo zero indica l’alba dei tempi ossia prima che qualsiasi Migrations sia stata applicata (probabilmente avremo un database vuoto). Se vogliamo rimuovere anche la fotografia di come era il nostro modello concettuale utilizziamo un altro comando. Remove.
Remove rimuove l’ultima Migrations. Vediamo ora come creare un vincolo di unicità con EFCore.
Se siamo in produzione l’utente con cui ci colleghiamo al database non avrà i permessi per generare istruzioni Create o Drop Table inoltre non sarà installato l’SDK per cui non possiamo usare il meccanismo delle Migrations. La slide mostra come generare istruzioni SQL con le migrations. Nel primo step InitialMigration è esclusa dallo script nel secondo caso le istruzioni sql prodotte sono solo quelle dei trigger.
RIEPILOGO DELLA SEZIONE
Ogni oggetto DbCommand, come il SqliteCommand, possiede 3 metodi specifici per aiutarci a ottenere il risultato voluto quando vogliamo inviare una query o un comando SQL.
- ExecuteReaderAsync è usato soprattutto quando inviamo query SQL al database perché ci restituisce un data reader che usiamo per leggere le informazioni dai set di risultati restituiti dal database (righe e colonne);
- ExecuteScalarAsync è anch’esso usato con query SQL ma è indicato quando il set di risultati restituito dal database è formato di una sola riga e una sola colonna. Perciò questo metodo restituisce un singolo valore che può essere un intero, una stringa o di altro tipo. È utile soprattutto quando vogliamo conteggiare il numero di righe in una tabella o ottenere l’ID dell’ultima riga inserita;
- ExecuteNonQueryAsync è usato per l’invio di comandi SQL che, tipicamente, non producono alcun set di risultati. Questo metodo restituisce perciò un numero intero che rappresenta il numero di righe interessate dal comando. Ad esempio, restituirà 1 se l’abbiamo usato per inviare un comando INSERT che si è concluso con successo.
AGGIUNGERE MODIFICARE ED ELIMINARE RIGHE
Vediamo un breve riepilogo di come persistere informazioni nel database.
- Con ADO.NET è sufficiente inviare i comandi SQL INSERT, UPDATE e DELETE. Ecco alcuni esempi che mostrano la sintassi per Sqlite ma valida anche per altre tecnologie database:
- INSERT INTO Courses (Title, Author) VALUES (‘Il mio corso’, ‘Mario Rossi’);
- UPDATE Courses SET Title=’Il mio nuovo corso’, Author=’Enzo Rossi’ WHERE Id=1;
- DELETE FROM Courses WHERE Id=1;
- Con Entity Framework Core abbiamo a disposizione dei metodi equivalenti come Add e Delete che informeranno il Change Tracker della nostra intenzione di voler inserire o eliminare un’entità. Per la modifica, invece, non è necessario invocare alcun metodo perché nel momento stesso in cui assegniamo una proprietà dell’entità, il Change Tracker riesce già a rendersi conto che ha subìto modifiche. Ogni cambiamento verrà persistito solo quando invochiamo il metodo SaveChangesAsync del DbContext. Ecco un esempio completo, in cui una nuova entità viene aggiunta, una viene modificata e un’altra eliminata.
- Course course1 = new Course(“Il mio corso”, “Mario Rossi”);
- Course course2 = await dbContext.Courses.FindAsync(2);
- Course course3 = await dbContext.Courses.FindAsync(3);
- dbContext.Add(course1); //Aggiungiamo
- course2.ChangeTitle(“Nuovo titolo”); //Modifichiamo
- dbContext.Remove(course3); //Eliminiamo
- await dbContext.SaveChangesAsync(); //E infine persistiamo le 3 entità tutte insieme
È possibile “ingannare” il Change Tracker sul fatto che un’entità debba essere aggiunta, modificata o eliminata. Possiamo manipolare lo stato di un’entità a piacimento in questo modo:
- dbContext.Entry(course).State = EntityState.Deleted; //Al SaveChangesAsync, l’entità verrà eliminata
EVITARE PERDITE DI DATI CON LA CONCORRENZA OTTIMISTICA
Se più utenti lavorano contemporaneamente allo stesso form di modifica può verificarsi che l’uno sovrascriva inavvertitamente le modifiche apportate dall’altro, senza che nessuno ne sia immediatamente consapevole.
Questo può succedere perché dopo che il form di modifica è stato caricato dal browser, continuerà a visualizzare le stesse informazioni anche se nel frattempo altri utenti hanno apportato modifiche. All’invio del form, potrebbero quindi essere persistite delle vecchie informazioni, sovrascrivendo quelle più aggiornate.
Per evitare questo problema:
- aggiungiamo una nuova colonna chiamata ad esempio RowVersion alla tabella del database;
- facciamo in modo che il contenuto della colonna venga rigenerato automaticamente ogni volta che la riga viene aggiornata. In base alla tecnologia database usata, questo si può ottenere con colonne di tipo apposito per questo scopo oppure con un trigger che si attiverà subito dopo l’inserimento o l’aggiornamento della riga;
- introduciamo il valore di RowVersion nel form, ad esempio in un campo nascosto dato che il valore non deve essere mostrato all’utente;
- Quando il form viene inviato, andiamo a persistere i dati solo se il valore di RowVersion che ci arriva dal form è ancora identico a quello che si trova sulla riga nel database. Altrimenti, se il valore è diverso, significa che la riga nel frattempo è stata aggiornata e perciò restituiamo un errore all’utente chiedendogli di ricaricare il form e ripetere le modifiche che aveva apportato.
PER IMPLEMENTARE QUESTO PUNTO:
- Con ADO.NET, dobbiamo semplicemente coinvolgere il valore di RowVersion nella clausola WHERE del comando UPDATE. Ad esempio:
- FormattableString command = $”UPDATE Courses SET Title={inputModel.Title} WHERE Id={inputModel.Id} AND RowVersion={inputModel.RowVersion}”
- Con Entity Framework Core, nel metodo OnModelCreating del DbContext indichiamo IsRowVersion() per la nuova proprietà.
- modelBuilder.Entity<Course>(entity =>
- {
- entity.Property(course => course.RowVersion).IsRowVersion();
- //Qui altro mapping
- }
Ora, nel servizio applicativo in cui persistiamo l’entità, dovremo usare la seguente sintassi.
- //Recuperiamo l’entità
- Course course = await dbContext.Courses.FindAsync(inputModel.Id);
- //Aggiorniamo i valori (ad esempio il titolo)
- course.ChangeTitle(inputModel.Title);
- //Impostiamo la RowVersion che abbiamo ricevuto dal form
- dbContext.Entry(course).Property(course => course.RowVersion).OriginalValue = inputModel.RowVersion;
- //Proviamo a persistere
- try
- {
- await dbContext.SaveChangesAsync();
- }
- catch (DbUpdateConcurrencyException)
- {
- //Se si verifica una DbUpdateConcurrencyException, sapremo che la RowVersion è cambiata
- //perciò solleviamo un’eccezione più significativa, che cattureremo dal Controller
- throw new OptimisticConcurrencyException();
- }
ELIMINARE LOGICAMENTE UNA RIGA CON LA SOFT-DELETE
Anziché eliminare fisicamente una riga dal database, se la situazione lo richiede, possiamo invece marcare la riga come eliminata logicamente. In questo modo la riga continuerà ad essere presente nel database e a mantenere tutte le sue relazioni con altre eventuali righe dipendenti presenti in altre tabelle.
La nostra applicazione, nell’estrarre i dati dal database, dovrà escludere le righe marcate come eliminate, così che non siano visualizzate negli elenchi e nelle pagine di dettaglio. Questa strategia, che non richiede l’eliminazione fisica dei dati, viene appunto denominata soft-delete.
Per implementarla, dobbiamo innanzitutto aggiungere una nuova colonna alla tabella del database che ci aiuti a distinguere le righe marcate come eliminate da quelle ancora attive. Ecco delle idee su come creare la colonna:
- Può ad esempio chiamarsi Deleted e ammettere i valori 0 o 1, dove 1 indica appunto l’avvenuta eliminazione;
- Oppure, potrebbe chiamarsi Status e ammettere vari valori stringa come Draft, Published e Deleted, dove solo Deleted indica l’avvenuta eliminazione mentre gli altri valori servono comunque a controllare la visibilità della riga nell’applicazione. Ad esempio, Draft potrebbe indicare che la riga deve essere mostrata solo all’utente che l’ha creata, mentre Published la renderebbe visibile a tutto il pubblico di utenti.
Per cui, quando l’utente vuol eliminare una riga, l’applicazione dovrà semplicemente limitarsi ad aggiornare il valore della nuova colonna che abbiamo aggiunto alla tabella. Ad esempio, l’eventuale colonna Deleted dovrà essere impostata su 1 o la colonna Status impostata su Deleted.
La parte più delicata consiste poi nell’assicurarci che l’applicazione escluda tutte le righe marcate come eliminate.
- Con ADO.NET, dovremo necessariamente aggiungere WHERE Status<>’Deleted’ ad ogni query o comando SQL inviato dalla nostra applicazione. Bisogna fare molta attenzione perché è facile dimenticarsene, soprattutto quando scriviamo una nuova query o comando. Per limitare questo rischio, potremmo creare una vista nel database, in modo che il filtro venga applicato in maniera centralizzata. Non tutte le tecnologie database permettono la creazione di viste aggiornabili, perciò dovremo continuare ad integrare la clausola WHERE almeno per i comandi UPDATE e DELETE;
- Con Entity Framework Core è molto più facile perché possiamo impostare un Global Query Filter che verrà automaticamente applicato a ogni query LINQ che rivolgeremo all’EntitySet. Il filtro va indicato nel metodo OnModelCreating del DbContext, usando il metodo HasQueryFilter, come si vede nell’esempio seguente.
- modelBuilder.Entity<Course>(entity =>
- {
- entity.HasQueryFilter(course => course.Status != CourseStatus.Deleted);
- //Qui altro mapping
- }
AGGIORNARE LA STRUTTURA DEL DATABASE CON LE MIGRATIONS
Quando vogliamo far evolvere la struttura del database, ad esempio per aggiungere o modificare una tabella o una colonna, possiamo farlo sfruttando le migration, una funzionalità offerta da Entity Framework Core.
Usare le migration ci porta a seguire l’approccio code-first. Questa è la sequenza delle attività che ci troveremo a svolgere:
- Prima modifichiamo il codice del modello concettuale, ad esempio aggiungendo una nuova proprietà alla classe Course o modificando il suo mapping nel DbContext;
- Poi aggiungiamo una migration al progetto eseguendo il comando dotnet ef migrations add NomeMigration. Verrà automaticamente generata una nuova migration che descriverà le modifiche apportate al punto 1;
- Infine eseguiamo il comando dotnet ef database update per chiedere ad Entity Framework Core di applicare la migration ovvero modificare la struttura del database in maniera coerente con quanto descritto dalla migration stessa.
Una migration è una classe C# che contiene i metodi Up e Down. Entrambi questi metodi definiscono un parametro di tipo MigrationBuilder che viene usato per descrivere le operazioni da compiere nei confronti del database, come aggiungere una colonna o creare una tabella. Inoltre può essere usato per inviare comandi SQL arbitrari.
- Il metodo Up serve a far evolvere la struttura del database e viene invocato da Entity Framework Core quando applichiamo la migration. Di solito qui vengono create tabelle e colonne;
- Il metodo Down serve a far devolvere la struttura del database, cioè viene invocato quando vogliamo annullare la migration dopo che è stata applicata. Di solito viene usato per eliminare tabelle e colonne, perciò può comportare la perdita di dati. Utile quando scopriamo che c’è un problema in una delle ultime migration applicate e vogliamo tornare ad una struttura precedente, per poi riapplicarla dopo averla corretta.
Entity Framework Core rispetta molto rigorosamente l’ordine con cui abbiamo aggiunto le migration al progetto. L’una segue sempre l’altra, come le stazioni ferroviarie o della metropolitana.
Per questo possiamo far evolvere o devolvere la struttura del database avanti e indietro lungo quest’unica direzione usando il comando dotnet ef database update, che permette anche di indicare il nome di una migration di destinazione. Supponendo che nessuna migration sia ancora stata applicata al database, ecco gli effetti prodotti dai seguenti comandi:
- dotnet ef database update tutte le migration vengono applicate, perciò verranno invocati i metodi Up di ciascuna migration, nel rigoroso ordine riportato nell’immagine, da sinistra verso destra. L’ultima migration applicata è dunque LessonVersion;
- dotnet ef database update LessonOrder la struttura del database devolve per tornare alla migration LessonOrder, perciò verrà invocato il metodo Down di LessonVersion;
- dotnet ef database update 0 vengono invocati i metodi Down di ogni migration applicata (le prime quattro) da destra verso sinistra, così che il database torni al suo stato originale, cioè privo di qualsiasi tabella;
- dotnet ef database update UniqueCourseTitle viene invocato il metodo Up della InitialMigration e poi il metodo Up della UniqueCourseTitle.
Se vogliamo eliminare dal progetto l’ultima migration creata, eseguiamo il comando dotnet ef migrations remove. Solo l’ultima migration può essere eliminata, purché non sia stata applicata al database. È importante NON eliminare a mano i file .cs delle migration dal progetto, ma usare sempre i comandi forniti.
LINK AL CODICE SU GITHUB
Scaricare il codice della sezione16 oppure il ramo master o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.
Scrivi un commento