EF Core – Datenbankschemaänderungen

EF Core – Datenbankschemaänderungen

Jedes Mal, wenn Sie die Definition der Datenbank ändern – vom Umbenennen einer Spalte bis zum Erstellen einer Tabelle – wird dies als Änderung des Datenbankschemas bezeichnet. Mit EF Core behandeln Sie Datenbankschemaänderungen mithilfe von Migrationen.

Wenn Sie die Datenbank zum ersten Mal erstellen, erstellen Sie eine Migration, die die anfängliche Definition der Datenbank enthält. Wenn Sie Schemaänderungen vornehmen, fügen Sie neue Migrationen hinzu und wenden sie zusätzlich zu den vorhandenen Migrationen an.

In diesem Artikel zeige ich Beispiele für das Durchlaufen des Datenbankschema-Änderungsprozesses in einigen verschiedenen Szenarien, einschließlich Fehlerszenarien, die eine Anpassung der Migration erfordern.

Hinweis:Ich verwende das dotnet ef-Tool zur Handhabung von Migrationen. Ich verwende die Befehlszeile – dotnet ef database update – zum Anwenden der Migration. Ich arbeite in einer Entwicklungsumgebung, daher dient dies der Einfachheit und Kürze.

Datenbankschema-Änderungsprozess

Im Folgenden finden Sie eine Liste der Schritte, die am Änderungsprozess des Datenbankschemas beteiligt sind:

  • Führen Sie die Schemaänderung im Code durch.
  • Erstellen Sie eine neue Migration.
  • Überprüfen Sie die Korrektheit des generierten Migrationsquellcodes.
  • Bei Problemen mit der Migration:
    • Teilen Sie Schemaänderungen in kleinere Migrationen auf -ODER- passen Sie die Migration an, um die Probleme zu beheben.
  • Wenden Sie die Migration an.
  • Korrektheit in der Datenbank überprüfen.

Jetzt zeige ich Beispiele für diesen Prozess in verschiedenen Szenarien.

Beispiel – Hinzufügen einer neuen Tabelle

Angenommen, Sie möchten eine neue Tabelle namens Shows hinzufügen .

Fügen Sie zuerst eine neue Modellklasse namens Show hinzu :

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)

Fügen Sie dann eine DbSet-Eigenschaft zu Ihrer DbContext-Klasse hinzu:

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)

Erstellen Sie die Migration:

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

Sehen Sie sich den generierten Migrationsquellcode in _Database_v1 an .cs und überprüfen Sie die Korrektheit:

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)

Es sieht richtig aus, also wenden Sie die Migration an:

dotnot ef database update
Code language: PowerShell (powershell)

Jetzt sehen Sie die Shows Tabelle in der Datenbank. Es ist eine gute Idee, die Tabellendefinition in der Datenbank zu überprüfen, nachdem die Migration angewendet wurde.

Beispiel – Falsche Migration, die zu Datenverlust führt, und wie sie behoben werden kann

Überprüfen Sie immer den generierten Migrationsquellcode. Dies kann nicht oft genug wiederholt werden. Der Migrationsgenerator ist nicht perfekt, wie ich weiter unten zeigen werde. Daher ist es immer notwendig, den Migrationscode zu überprüfen.

Angenommen, Sie haben Filme Tabelle und möchten die folgenden Schemaänderungen anwenden:

  • Änderung des Spaltennamens.
  • Neue Spalte hinzufügen.
  • Eine Spalte löschen.

Mal sehen, was passiert, wenn Sie versuchen, alle diese Änderungen gleichzeitig anzuwenden.

Wenden Sie zuerst die Schemaänderungen auf den Film an Modell:

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)

Generieren Sie dann die Migration:

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

Möglicherweise bemerken Sie die folgende Warnung (große rote Flagge):

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

Sehen Sie sich den generierten Migrationsquellcode in _Database_v3.cs an , und achten Sie genau auf die hervorgehobenen Teile:

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)

Die falsche Spalte wurde gelöscht – ReleaseYear statt RuntimeMinutes – und es hat die falsche Spalte umbenannt – RuntimeMinutes statt ReleaseYear .

Da die Migration falsch ist, entfernen Sie sie:

dotnet ef migrations remove
Code language: PowerShell (powershell)

Wenn Ihre Tabelle vorhandene Daten enthielt und Sie diese Migration tatsächlich angewendet haben, hätten Sie einen irreparablen Datenverlust. Sie würden am Ende eine Spalte namens YearOfRelease erhalten das hat die RuntimeMinutes Daten darin.

Dieser irreparable Datenverlust mag in einer Entwicklungsumgebung keine große Sache sein, aber denken Sie daran, dass Sie die Migration schließlich in einer Produktionsumgebung anwenden werden. Aus diesem Grund müssen Sie den generierten Migrationscode vorher immer noch einmal überprüfen es anwenden.

Um Datenverlust zu vermeiden, minimieren Sie die Anzahl der Schemaänderungen pro Migration

Wie oben gezeigt, können generierte Migrationen völlig falsch sein und zu Datenverlust führen.

Eine einfache Lösung besteht darin, mehrere kleine Migrationen zu erstellen. Anstatt zu versuchen, viele Schemaänderungen in einer Migration zu kombinieren, nehmen Sie nur Schemaänderungen auf, die zusammenpassen. Sie können durch Versuch und Irrtum herausfinden, welche Änderungen zusammenpassen.

Das folgende Beispiel zeigt diesen Ansatz.

Kleine Migration 1

Versuchen wir, diese beiden Schemaänderungen zu kombinieren:

  • Änderung des Spaltennamens.
  • Neue Spalte hinzufügen.

Nehmen Sie zuerst die Änderungen im Film vor Modell:

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)

Generieren Sie dann die Migration:

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

Sehen Sie sich den generierten Migrationsquellcode in _Database_v3.cs an :

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)

Diesmal ist der Migrationsquellcode korrekt. Es benennt das ReleaseYear um Spalte zu YearOfRelease und Hinzufügen des neuen BoxOfficeRevenue Spalte.

Kleine Migration 2

Die verbleibende Schemaänderung, die wir vornehmen müssen, ist Eine Spalte löschen.

Übernehmen Sie diese Änderung im Film Modell:

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)

Erstellen Sie dann eine neue Migration:

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

Sehen Sie sich den generierten Migrationsquellcode _Database_v4.cs an :

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)

Das ist richtig. Es lässt die RuntimeMinutes fallen Spalte.

Wenden Sie die beiden ausstehenden Migrationen an

Die beiden kleinen Migrationen wurden erstellt und auf Korrektheit überprüft. Beide sind ausstehende Migrationen.

Sehen Sie sich die Liste der Migrationen an:

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)

Wenden Sie nun die beiden ausstehenden Migrationen an:

dotnet ef database update
Code language: PowerShell (powershell)

Beachten Sie, dass dies beide Migrationen angewendet hat

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

Da die Schemaänderungen separat durchgeführt wurden, waren die generierten Migrationen korrekt und es gab keinen Datenverlust.

Was passiert, wenn Sie eine Tabelle mit Daten ändern?

Es gibt viele verschiedene Schemaänderungen, die Sie vornehmen können. Die meisten davon können problemlos auf Tabellen mit bestehenden Daten angewendet werden. Riskieren Sie es jedoch nicht – testen Sie Ihre Migrationen immer in einer Entwicklungsumgebung anhand von Tabellen, die Daten enthalten.

Abgesehen davon gibt es einige Schemaänderungen, die nicht auf eine Tabelle mit Daten angewendet werden können. Wenn Sie auf diese Situation stoßen, können Sie möglicherweise die Migration anpassen, um das Problem zu lösen. Ich werde unten zwei Beispiele dafür zeigen.

Ändern einer Nullable-Spalte in eine Nicht-Nullable

Wenn Sie versuchen, eine Nullable-Spalte so zu ändern, dass sie keine Nullwerte zulässt, und die Tabelle bereits NULL-Werte in dieser Spalte enthält, wird sie von der generierten Migration nicht ordnungsgemäß verarbeitet. Sie werden auf diesen Fehler stoßen:

Um dieses Problem zu lösen, können Sie die Migration anpassen, indem Sie Nullen auf einen Standardwert aktualisieren, bevor die Spalten geändert werden. Ich werde unten ein vollständiges Beispiel dieses Szenarios zeigen.

Beispiel dafür, wie eine Nullable Nullwerte nicht zulässt

Der Direktor Spalte ist derzeit nullable. Um es nicht nullable zu machen, fügen Sie das Attribut [Erforderlich] hinzu:

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)

Erstellen Sie die Migration für diese Änderung:

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

Sehen Sie sich den generierten Migrationsquellcode in _Database_v5.cs an und passen Sie es an, indem Sie eine UPDATE-Anweisung mit migrationBuilder.Sql():

ausführen
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)

Wenden Sie die Migration an:

dotnet ef database update
Code language: PowerShell (powershell)

Dies konnte die Migration ohne Probleme anwenden, da es alle Nullen durch leere Zeichenfolgen ersetzte und dann die Spalte so änderte, dass sie keine Nullen zulässt.

Reduzieren der Länge einer String-Spalte

Angenommen, Sie haben vorhandene Zeichenfolgen mit einer Länge von 50 Zeichen und möchten die maximale Länge dieser Zeichenfolgenspalte auf 40 Zeichen ändern. Wenn Sie versuchen, diese Schemaänderung anzuwenden, wird der folgende Fehler angezeigt:

Stellen Sie zunächst sicher, dass Sie mit dem Abschneiden der vorhandenen Daten in der Tabelle einverstanden sind.

Sie können dieses Problem lösen, indem Sie die Migration so anpassen, dass die Zeichenfolgenspalte vor der Spaltenänderung abgeschnitten wird.

Haftungsausschluss:Dies führt zu Datenverlust, da Sie die Zeichenfolgenspalte absichtlich abschneiden. Tun Sie dies nicht, wenn Sie keine Daten verlieren möchten.

Beispiel für die Reduzierung der Länge einer Zeichenfolgenspalte

Ändern Sie zunächst das Attribut [MaxLength] für die Beschreibung Spalte:

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)

Erstellen Sie dann eine neue Migration:

dotnet ef migrations add Database_v6

Sehen Sie sich nun den generierten Migrationsquellcode in _Database_v6.c an s und passen Sie es an, indem Sie eine UPDATE-Anweisung mit migrationBuilder.Sql():

ausführen
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)

Wenden Sie die Migration an:

dotnet ef database update
Code language: PowerShell (powershell)

Dadurch wurde die Migration erfolgreich angewendet, indem zuerst die Zeichenfolgenspalte auf die reduzierte Länge gekürzt und dann die Länge der Spalte geändert wurde.