EF Core:modifiche allo schema del database

EF Core:modifiche allo schema del database

Ogni volta che si modifica la definizione del database, dalla ridenominazione di una colonna alla creazione di una tabella, si parla di modifica dello schema del database. Con EF Core gestisci le modifiche allo schema del database usando le migrazioni.

Quando crei il database per la prima volta, crei una migrazione che contiene la definizione iniziale del database. Quando apporti modifiche allo schema, aggiungi nuove migrazioni e le applichi sopra le migrazioni esistenti.

In questo articolo mostrerò esempi di come passare attraverso il processo di modifica dello schema del database in alcuni scenari diversi, inclusi scenari di errore che richiedono la personalizzazione della migrazione.

Nota:utilizzerò lo strumento dotnet ef per gestire le migrazioni. Userò la riga di comando – dotnet ef database update – per applicare la migrazione. Sto lavorando in un ambiente di sviluppo, quindi questo è per semplicità e brevità.

Processo di modifica dello schema del database

Di seguito è riportato l'elenco dei passaggi coinvolti nel processo di modifica dello schema del database:

  • Apporta la modifica dello schema nel codice.
  • Crea una nuova migrazione.
  • Verifica la correttezza del codice sorgente della migrazione generato.
  • Se ci sono problemi con la migrazione:
    • Dividi le modifiche dello schema in migrazioni più piccole -OPPURE- personalizza la migrazione per correggere i problemi.
  • Applica la migrazione.
  • Verifica la correttezza nel database.

Ora mostrerò esempi di come eseguire questo processo in vari scenari.

Esempio:aggiunta di una nuova tabella

Supponiamo che tu voglia aggiungere una nuova tabella chiamata Spettacoli .

Innanzitutto, aggiungi una nuova classe modello chiamata Mostra :

using System.ComponentModel.DataAnnotations;

public class Show
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	[MaxLength(500)]
	public string Description { get; set; }

	[Required]
	public int NumberOfEpisodes { get; set; }

	[Required]
	public int NumberOfSeasons { get; set; }
	
	[Required]
	public int FirstYear { get; set; }
	
	public int? LastYear { get; set; }
}
Code language: C# (cs)

Quindi aggiungi una proprietà DbSet alla tua classe DbContext:

public class StreamingServiceContext : DbContext
{
	private readonly string ConnectionString;
	public StreamingServiceContext(string connectionString)
	{
		ConnectionString = connectionString;
	}
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		optionsBuilder.UseSqlServer(ConnectionString);
	}
	public DbSet<Movie> Movies { get; set; }
	public DbSet<Show> Shows { get; set; }
}
Code language: C# (cs)

Crea la migrazione:

dotnet ef migrations add Database_v1
Code language: PowerShell (powershell)

Dai un'occhiata al codice sorgente della migrazione generato in _Database_v1 .cs e verifica la correttezza:

public partial class Database_v1 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.CreateTable(
			name: "Shows",
			columns: table => new
			{
				Id = table.Column<int>(type: "int", nullable: false)
					.Annotation("SqlServer:Identity", "1, 1"),
				Name = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
				Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
				NumberOfEpisodes = table.Column<int>(type: "int", nullable: false),
				NumberOfSeasons = table.Column<int>(type: "int", nullable: false),
				FirstYear = table.Column<int>(type: "int", nullable: false),
				LastYear = table.Column<int>(type: "int", nullable: true)
			},
			constraints: table =>
			{
				table.PrimaryKey("PK_Shows", x => x.Id);
			});
	}
	//Down() not shown
}
Code language: C# (cs)

Sembra corretto, quindi applica la migrazione:

dotnot ef database update
Code language: PowerShell (powershell)

Ora vedrai gli Programmi tabella nel database. È una buona idea ricontrollare la definizione della tabella nel database dopo l'applicazione della migrazione.

Esempio:migrazione errata che porta alla perdita di dati e come risolverlo

Controllare sempre il codice sorgente della migrazione generato. Questo non può essere ripetuto abbastanza. Il generatore di migrazione non è perfetto, come mostrerò di seguito. Pertanto, è sempre necessario ricontrollare il codice di migrazione.

Supponiamo che tu abbia un Film tabella e desideri applicare le seguenti modifiche allo schema:

  • Cambia nome colonna.
  • Aggiungi una nuova colonna.
  • Rilascia una colonna.

Vediamo cosa succede se provi ad applicare tutte queste modifiche contemporaneamente.

Innanzitutto, applica le modifiche allo schema al Film modello:

public class Movie
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	public int YearOfRelease { get; set; } //changed name

	[Required]
	[MaxLength(500)]
	public string Description { get; set; }

	//dropped column
	//[Required]
	//public int RuntimeMinutes { get; set; }

	[MaxLength(100)]
	public string Director { get; set; }

	[Required]
	public decimal BoxOfficeRevenue { get; set; } //added new column
}
Code language: C# (cs)

Quindi genera la migrazione:

dotnet ef migrations add Database_v3
Code language: PowerShell (powershell)

Potresti notare il seguente avviso (grande bandiera rossa):

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.Code language: plaintext (plaintext)

Dai un'occhiata al codice sorgente della migrazione generato in _Database_v3.cs e presta molta attenzione alle parti evidenziate:

public partial class Database_v3 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.DropColumn(
			name: "ReleaseYear",
			table: "Movies");

		migrationBuilder.RenameColumn(
			name: "RuntimeMinutes",
			table: "Movies",
			newName: "YearOfRelease");

		migrationBuilder.AddColumn<decimal>(
			name: "BoxOfficeRevenue",
			table: "Movies",
			type: "decimal(18,2)",
			nullable: false,
			defaultValue: 0m);
	}
	
	//not showing Down()
}
Code language: C# (cs)

È stata eliminata la colonna sbagliata:ReleaseYear invece di RuntimeMinutes – e ha rinominato la colonna sbagliata – RuntimeMinutes invece di Anno di rilascio .

Poiché la migrazione è errata, rimuovila:

dotnet ef migrations remove
Code language: PowerShell (powershell)

Se la tua tabella contiene dati esistenti e hai effettivamente applicato questa migrazione, avresti una perdita di dati irreparabile. Finiresti con una colonna chiamata YearOfRelease che ha i RuntimeMinutes dati in esso contenuti.

Questa perdita irreparabile di dati potrebbe non essere un grosso problema in un ambiente di sviluppo, ma ricorda che alla fine applicherai la migrazione in un ambiente di produzione. Questo è il motivo per cui devi sempre ricontrollare il codice di migrazione generato prima applicandolo.

Per prevenire la perdita di dati, riduci al minimo il numero di modifiche allo schema per migrazione

Come mostrato sopra, le migrazioni generate possono essere totalmente errate e portare alla perdita di dati.

Una soluzione semplice consiste nel creare migrazioni multiple e di piccole dimensioni. Invece di provare a combinare molte modifiche allo schema in un'unica migrazione, includi solo le modifiche allo schema che possono essere integrate. Puoi capire quali modifiche possono essere combinate per tentativi ed errori.

L'esempio seguente mostra questo approccio.

Migrazione ridotta 1

Proviamo a combinare queste due modifiche allo schema:

  • Cambia nome colonna.
  • Aggiungi una nuova colonna.

Innanzitutto, apporta le modifiche nel Film modello:

public class Movie
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	public int YearOfRelease { get; set; } //changed name

	[Required]
	[MaxLength(500)]
	public string Description { get; set; }

	[Required]
	public int RuntimeMinutes { get; set; }

	[MaxLength(100)]
	public string Director { get; set; }

	[Required]
	public decimal BoxOfficeRevenue { get; set; } //added new column
}
Code language: C# (cs)

Quindi genera la migrazione:

dotnet ef migrations add Database_v3
Code language: PowerShell (powershell)

Dai un'occhiata al codice sorgente della migrazione generato in _Database_v3.cs :

public partial class Database_v3 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.RenameColumn(
			name: "ReleaseYear",
			table: "Movies",
			newName: "YearOfRelease");

		migrationBuilder.AddColumn<decimal>(
			name: "BoxOfficeRevenue",
			table: "Movies",
			type: "decimal(18,2)",
			nullable: false,
			defaultValue: 0m);
	}
//Down() not shown
}
Code language: C# (cs)

Questa volta il codice sorgente della migrazione è corretto. Sta rinominando ReleaseYear colonna a YearOfRelease e aggiungendo il nuovo BoxOfficeRevenue colonna.

Migrazione ridotta 2

La restante modifica dello schema che dobbiamo fare è Rilasciare una colonna.

Applica questa modifica nel Film modello:

public class Movie
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	public int YearOfRelease { get; set; }

	[Required]
	[MaxLength(500)]
	public string Description { get; set; }

	//dropped column
	//[Required]
	//public int RuntimeMinutes { get; set; }

	[MaxLength(100)]
	public string Director { get; set; }

	[Required]
	public decimal BoxOfficeRevenue { get; set; }
}
Code language: C# (cs)

Quindi genera una nuova migrazione:

dotnet ef migrations add Database_v4
Code language: PowerShell (powershell)

Dai un'occhiata al codice sorgente della migrazione generato _Database_v4.cs :

public partial class Database_v4 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.DropColumn(
			name: "RuntimeMinutes",
			table: "Movies");
	}

	//Down() not shown
}
Code language: C# (cs)

Questo è corretto. Sta eliminando i RuntimeMinutes colonna.

Applica le due migrazioni in sospeso

Le due piccole migrazioni sono state create e verificate per correttezza. Sono entrambe migrazioni in sospeso.

Dai un'occhiata all'elenco delle migrazioni:

dotnet ef migrations list
Code language: PowerShell (powershell)
20210314133726_Database_v0
20210315113855_Database_v1
20210316112804_Database_v2
20210316123742_Database_v3 (Pending)
20210316124316_Database_v4 (Pending)
Code language: plaintext (plaintext)

Ora applica le due migrazioni in sospeso:

dotnet ef database update
Code language: PowerShell (powershell)

Nota che questo ha applicato entrambe le migrazioni

Applying migration '20210316123742_Database_v3'.
Applying migration '20210316124316_Database_v4'.Code language: plaintext (plaintext)

Poiché le modifiche allo schema sono state eseguite separatamente, le migrazioni generate erano corrette e non si è verificata alcuna perdita di dati.

Cosa succede quando modifichi una tabella che contiene dati?

Ci sono molte diverse modifiche allo schema che puoi fare. La maggior parte di essi può essere applicata senza problemi a tabelle con dati esistenti. Non rischiare però:testa sempre le tue migrazioni in un ambiente di sviluppo rispetto a tabelle che contengono dati.

Detto questo, ci sono alcune modifiche allo schema che non verranno applicate a una tabella con dati al loro interno. Quando ti imbatti in questa situazione, potresti essere in grado di personalizzare la migrazione per risolvere il problema. Mostrerò due esempi di questo di seguito.

Modifica di una colonna nullable in non nullable

Quando si tenta di modificare una colonna nullable per non consentire valori null e la tabella ha già valori NULL in quella colonna, la migrazione generata non la gestirà correttamente. Ti imbatterai in questo errore:

Per risolvere questo problema, puoi personalizzare la migrazione aggiornando i valori null a un valore predefinito prima che cambi la colonna. Mostrerò un esempio completo di questo scenario di seguito.

Esempio di come rendere nullable non consentire null

Il direttore la colonna è attualmente nullable. Per renderlo non nullable, aggiungi l'attributo [Obbligatorio]:

public class Movie
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	public int YearOfRelease { get; set; }

	[Required]
	[MaxLength(500)]
	public string Description { get; set; }

	[Required] //required = doesn't allow nulls
	[MaxLength(100)]
	public string Director { get; set; }

	[Required]
	public decimal BoxOfficeRevenue { get; set; }
}
Code language: C# (cs)

Crea la migrazione per questa modifica:

dotnet ef migrations add Database_v5
Code language: PowerShell (powershell)

Dai un'occhiata al codice sorgente della migrazione generato in _Database_v5.cs e personalizzalo eseguendo un'istruzione UPDATE con migrationBuilder.Sql():

public partial class Database_v5 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.Sql(@"UPDATE Movies SET Director = '' WHERE Director IS NULL");

		migrationBuilder.AlterColumn<string>(
			name: "Director",
			table: "Movies",
			type: "nvarchar(100)",
			maxLength: 100,
			nullable: false,
			defaultValue: "",
			oldClrType: typeof(string),
			oldType: "nvarchar(100)",
			oldMaxLength: 100,
			oldNullable: true);
	}

	//Down() not shown
}
Code language: C# (cs)

Applica la migrazione:

dotnet ef database update
Code language: PowerShell (powershell)

Questo è stato in grado di applicare la migrazione senza problemi, perché ha sostituito tutti i null con stringhe vuote e quindi ha modificato la colonna per non consentire i null.

Ridurre la lunghezza di una colonna di stringa

Supponiamo che tu abbia stringhe esistenti lunghe 50 caratteri e desideri modificare la lunghezza massima di questa colonna di stringhe a 40 caratteri. Quando provi ad applicare questa modifica dello schema, ti imbatterai nel seguente errore:

Innanzitutto, assicurati di essere d'accordo con il troncamento dei dati esistenti nella tabella.

Puoi risolvere questo problema personalizzando la migrazione per troncare la colonna della stringa prima che esegua la modifica della colonna.

Disclaimer:in questo modo si verificherà una perdita di dati perché troncherai intenzionalmente la colonna della stringa. Non farlo se non vuoi perdere dati.

Esempio di riduzione della lunghezza di una colonna di stringa

Innanzitutto, modifica l'attributo [MaxLength] per la Descrizione colonna:

public class Movie
{
	[Key]
	public int Id { get; set; }

	[Required]
	[MaxLength(500)]
	public string Name { get; set; }

	[Required]
	public int YearOfRelease { get; set; }

	[Required]
	[MaxLength(30)] //reduced from 500 to 30
	public string Description { get; set; }

	[Required]
	[MaxLength(100)]
	public string Director { get; set; }

	[Required]
	public decimal BoxOfficeRevenue { get; set; }
}
Code language: C# (cs)

Quindi crea una nuova migrazione:

dotnet ef migrations add Database_v6

Ora dai un'occhiata al codice sorgente della migrazione generato in _Database_v6.c se personalizzalo eseguendo un'istruzione UPDATE con migrationBuilder.Sql():

public partial class Database_v6 : Migration
{
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.Sql(@"UPDATE Movies SET Description = LEFT(Description, 30) WHERE LEN(Description) > 30");

		migrationBuilder.AlterColumn<string>(
			name: "Description",
			table: "Movies",
			type: "nvarchar(30)",
			maxLength: 30,
			nullable: false,
			oldClrType: typeof(string),
			oldType: "nvarchar(500)",
			oldMaxLength: 500);
	}

	//Down() not shown
}
Code language: C# (cs)

Applica la migrazione:

dotnet ef database update
Code language: PowerShell (powershell)

Ciò ha applicato correttamente la migrazione troncando prima la colonna della stringa alla lunghezza ridotta, quindi ha modificato la lunghezza della colonna.