Finalmente sono riuscito a trovare qualche minuto per buttare giù qualche riga a riguardo di EF Code First Migrations (da qualche giorno disponibile in beta 1). Per chi usa l’approccio Code First l’aggiornamento del modello e della base di dati sottostante (soprattutto quando contiene dati) è un grosso problema. Proviamo a testare il funzionamento del “pacchetto” su un modello molto semplice come il seguente, in un progetto console C#:
public class OfficeContext : DbContext
{
public OfficeContext()
: base("OfficeDB")
{
}
public DbSet<Employee> Employees { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
static OfficeContext()
{
Database.SetInitializer<OfficeContext>(null);
}
}
public class Employee
{
public int Id { get; set; }
public String Name { get; set; }
public String Surnamte { get; set; }
public String Role { get; set; }
}
Aggiungiamo al progetto un file App.Config con una stringa di connessione simile alla seguente:
<add name="OfficeDB" connectionString="Data Source=(local)\SQLEXPRESS;Initial Catalog=OfficeDB;Integrated Security=True;" providerName="System.Data.SqlClient"/>
Per come abbiamo configurato il DBContext nessun database verrà generato, anzi, a runtime avremo un’eccezione di questo tipo:
Procediamo con l’installazione via NuGet del Package EntityFramework.Migrations (Code First Migrations Beta 1, ver. 0.8.0.0):
Durante il processo di installazione viene automaticamente aggiunta al progetto una nuova cartella, “Migrations” contenente il file Configuration.cs (derivata da DbMigrationsConfiguration<T> dove T è la classe OfficeContext) nel quale possiamo specificare, ad esempio, se utilizzare la “migrazione automatica del modello” (vedremo in seguito) tramite la proprietà AutomaticMigrationsEnabled (false di default), se continuare o meno una migrazione dello schema anche in presenza di perdita dei dati, AutomaticMigrationDataLossAllowed, o se sollevare un’eccezione. Inoltre tramite l’override di Seed è possibile specificare se aggiungere dopo la generazione\migrazione dello schema, dei dati al database (avendo avuto qualche problema nei test con l’estensione AddOrUpdate preferisco utilizzare codice SQL per il momento ) . La classe Configuration sarà così definita:
internal sealed class Configuration : DbMigrationsConfiguration<OfficeContext>
{
protected override void Seed(OfficeContext context)
{
base.Seed(context);
}
public Configuration()
{
AutomaticMigrationsEnabled = false;
AutomaticMigrationDataLossAllowed = false;
}
}
Ora, per eseguire la creazione del database, dobbiamo aggiungere “a scaffale” la nostra prima “migrazione”(ovvero la generazione del database): così facendo, oltre a richiamare i metodi per la generazione del database, potremmo eseguire nel tempo degli Upgrade e Downgrade utilizzando i vari piani di migrazione che andremo a creare: se uniamo a quanto detto TFS, penso non ci sia altro da aggiungere. Nella console di NuGet (Package Manager Console) digitiamo : Add-Migration [parametro] dove parametro è un etichetta che identifica in modo univoco la migrazione che stiamo aggiungendo. Nel nostro caso parametro=OfficeDBFirstMigration. Se tutto procede senza errori, la schermata della console di NuGet dovrebbe essere simile alla seguente:
Al progetto viene aggiunto un un file xxxx_OfficeDBFirstMigration.cs contenente uno “Snapshot” del modello Code First del database da creare definito dalla classe DbContext (OfficeContext per il caso specifico). Per generare il database, digitiamo il comando Update-Database (che prende in considerazione l’ultimo file di migrazione “Pending”, “a scaffale”), eventualmente aggiungendo il parametro –Verbose per visualizzare i comandi SQL generati, oppure il parametro –Script per generare un file .sql da scambiare con gli altri membri del team o eventualmente da aggiungere su TFS:
Utilizzando SQL Management Studio possiamo vedere come il database sia stato creato secondo le nostre specifiche (le colonne sono state aggiunge utilizzando le convenzioni di EF):
Prima di continuare, facciamo un passo indietro e “spulciamo” il contenuto della classe OfficeDBFirstMigration generata dal comando Add-Migration:
public partial class OfficeDBFirstMigration : DbMigration
{
public override void Up()
{
CreateTable(
"Employees",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
Surname = c.String(),
Role = c.String(),
})
.PrimaryKey(t => t.Id);
CreateTable(
"EdmMetadata",
c => new
{
Id = c.Int(nullable: false, identity: true),
ModelHash = c.String(),
})
.PrimaryKey(t => t.Id);
Sql("INSERT INTO Employees ([Name],[Surname],[Role]) VALUES ('Pietro','Libro','Art Director')");
Sql("INSERT INTO Employees ([Name],[Surname],[Role]) VALUES ('Mario','Rossi','Administrator')");
Sql("INSERT INTO Employees ([Name],[Surname],[Role]) VALUES ('Giulio','Verdi','Store Manager')");
}
public override void Down()
{
DropTable("EdmMetadata");
DropTable("Employees");
}
}
Nella classe è stato generato l’override dei metodi Up e Down (sembra il motivo di una canzone di qualche anno fa ) . Up, come è facile intuire, contiene il codice eseguito per l’esecuzione dell’upgrade dello schema della base dati, Down, il codice eseguito per effettuare il downgrade ad una versione precedente della base dati. La lettura del codice è abbastanza semplice, ed in questo punto possiamo intervenire per specificare manualmente (ove necessario) quanto non automaticamente generato da VS: ad esempio avremmo potuto aggiungere del codice per specificare degli indici (automatico per le chiavi primarie, Id nello specifico) o delle Foreign Key. Inoltre possiamo aggiungere del codice SQL personalizzato utilizzando il metodo SQL (…). Utilizziamo questo metodo per aggiungere tre righe alla tabella Employees.
Eseguendo il nostro progetto , il risultato che otteniamo è mostrato figura:
Modifichiamo il modello dati, nello specifico l’entità Employee: aggiungiamo la colonna CardID (di tipo stringa) contenente il numero identificativo stampato sul badge di ogni dipendente, ed impostiamo tramite override dell’OnModelBuilder la lunghezza massima del campo a 20 caratteri. Inizialmente tutti i dipendenti dovranno avere un identificativo del tipo “00000-00000”. Procediamo con il comando Add-Migration OfficeDBSecondMigration, per aggiungere un nuovo piano di migrazione, ovvero un nuovo file .cs del tipo xxxxxx_OfficeDBSecondMigration.cs, che andiamo immediatamente ad ispezionare per aggiungere del codice personalizzato (per impostare il default value) prima di procedere con l’aggiornamento:
public partial class OfficeDBsecondMigration : DbMigration
{
public override void Up()
{
AddColumn("Employees", "CardID", c => c.String(nullable: false, defaultValue: "00000-00000", maxLength:20));
}
public override void Down()
{
DropColumn("Employees", "CardID");
}
}
(Nota: invertendo l’ordine tra defaultValue e maxLength sull’ambiente di test viene sollevata un’eccezione , d’altra parte non sarebbe una beta ). Procediamo con l’aggiornamento (Update-Database). Vediamo cosa è successo:
Quello che ci aspettavamo. Utilizzando solo Code First, questo processo di aggiornamento sarebbe risultato distruttivo e comunque avrebbe richiesto un certo effort di modifiche “a manina”. Prima di concludere cerchiamo di aumentare la complessità del modello aggiungendo una tabella Roles ed una relazione uno-a-molti tra le entità Employee e Role:
Mettiamo in pending il nuovo schema dati (Add-Migration OfficeDBThirdMigration):
public partial class OfficeDBThirdMigration : DbMigration
{
public override void Up()
{
CreateTable(
"Roles",
c => new
{
Id = c.Int(nullable: false, identity: true),
Description = c.String(),
})
.PrimaryKey(t => t.Id);
AddColumn("Employees", "Role_Id", c => c.Int());
AddForeignKey("Employees", "Role_Id", "Roles", "Id");
CreateIndex("Employees", "Role_Id");
DropColumn("Employees", "Role");
Sql("INSERT INTO Roles (Description) VALUES ('Administrator')");
Sql("INSERT INTO Roles (Description) VALUES ('Store Manager')");
Sql("INSERT INTO Roles (Description) VALUES ('Art Director')");
Sql("UPDATE Employees SET Role_ID = 1 WHERE Name='Mario' AND Surname ='Rossi'");
Sql("UPDATE Employees SET Role_ID = 2 WHERE Name='Giulio' AND Surname ='Verdi'");
Sql("UPDATE Employees SET Role_ID = 3 WHERE Name='Pietro' AND Surname ='Libro'");
}
public override void Down()
{
AddColumn("Employees", "Role", c => c.String());
DropIndex("Employees", new[] { "Role_Id" });
DropForeignKey("Employees", "Role_Id", "Roles", "Id");
DropColumn("Employees", "Role_Id");
DropTable("Roles");
}
}
A cui abbiamo aggiunto del codice SQL custom. Un ultimo Update-Database ed il gioco è fatto:
Il risultato ovviamente non cambia
Per eseguire l’upgrade-downgrade del modello dati è sufficiente invocare il metodo Update-Database –targetmigration [parametro] dove [parametro] è uno dei nomi utilizzati in precedenza (ad esempio OfficeDBFirstMigration ). Vedremo nel prossimo post il funzionamento della modalità “automatica” di migrazione.