EF Core:cambios en el esquema de la base de datos

EF Core:cambios en el esquema de la base de datos

Cada vez que cambia la definición de la base de datos, desde cambiar el nombre de una columna hasta crear una tabla, se denomina cambio de esquema de la base de datos. Con EF Core, se ocupa de los cambios en el esquema de la base de datos mediante el uso de migraciones.

Cuando crea la base de datos por primera vez, crea una migración que contiene la definición inicial de la base de datos. A medida que realiza cambios en el esquema, agrega nuevas migraciones y las aplica sobre las migraciones existentes.

En este artículo, mostraré ejemplos del proceso de cambio del esquema de la base de datos en algunos escenarios diferentes, incluidos escenarios de error que requieren personalizar la migración.

Nota:Usaré la herramienta dotnet ef para manejar las migraciones. Usaré la línea de comando (actualización de la base de datos dotnet ef) para aplicar la migración. Estoy trabajando en un entorno de desarrollo, así que esto es por simplicidad y brevedad.

Proceso de cambio de esquema de base de datos

La siguiente es la lista de pasos involucrados en el proceso de cambio de esquema de base de datos:

  • Hacer el cambio de esquema en el código.
  • Cree una nueva migración.
  • Verifique que el código fuente de migración generado sea correcto.
  • Si hay problemas con la migración:
    • Dividir los cambios de esquema en migraciones más pequeñas -O- personalizar la migración para corregir los problemas.
  • Aplicar la migración.
  • Verificar la corrección en la base de datos.

Ahora mostraré ejemplos de este proceso en varios escenarios.

Ejemplo:Agregar una nueva tabla

Supongamos que desea agregar una nueva tabla llamada Espectáculos .

Primero, agregue una nueva clase de modelo llamada Mostrar :

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)

Luego agregue una propiedad DbSet a su clase 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 migración:

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

Eche un vistazo al código fuente de migración generado en _Database_v1 .cs y verificar la corrección:

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)

Parece correcto, así que aplica la migración:

dotnot ef database update
Code language: PowerShell (powershell)

Ahora verás los Programas tabla en la base de datos. Es una buena idea verificar dos veces la definición de la tabla en la base de datos después de aplicar la migración.

Ejemplo:migración incorrecta que conduce a la pérdida de datos y cómo solucionarlo

Compruebe siempre el código fuente de migración generado. Esto no se puede repetir lo suficiente. El generador de migración no es perfecto, como mostraré a continuación. Por lo tanto, siempre es necesario verificar dos veces el código de migración.

Digamos que tienes una Películas tabla y desea aplicar los siguientes cambios de esquema:

  • Cambio de nombre de columna.
  • Añadir una nueva columna.
  • Soltar una columna.

Veamos qué sucede si intenta aplicar todos estos cambios al mismo tiempo.

Primero, aplique los cambios de esquema a la Película modelo:

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)

Luego genera la migración:

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

Es posible que observe la siguiente advertencia (gran bandera roja):

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

Eche un vistazo al código fuente de migración generado en _Database_v3.cs y presta mucha atención a las partes resaltadas:

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)

Descartó la columna incorrecta:ReleaseYear en lugar de RuntimeMinutes – y cambió el nombre de la columna incorrecta – RuntimeMinutes en lugar de ReleaseYear .

Dado que la migración es incorrecta, elimínela:

dotnet ef migrations remove
Code language: PowerShell (powershell)

Si su tabla tuviera datos existentes y realmente aplicara esta migración, tendría una pérdida de datos irreparable. Terminaría con una columna llamada YearOfRelease que tiene los RuntimeMinutes datos en él.

Esta pérdida de datos irreparable puede no ser un gran problema en un entorno de desarrollo, pero recuerde que eventualmente aplicará la migración en un entorno de producción. Es por eso que siempre debe verificar dos veces el código de migración generado antes aplicándolo.

Para evitar la pérdida de datos, minimice la cantidad de cambios de esquema por migración

Como se muestra arriba, las migraciones generadas pueden ser totalmente incorrectas y provocar la pérdida de datos.

Una solución simple es crear varias migraciones pequeñas. En lugar de intentar combinar muchos cambios de esquema en una sola migración, incluya solo cambios de esquema que puedan ir juntos. Puede averiguar qué cambios pueden ir juntos por ensayo y error.

El siguiente ejemplo muestra este enfoque.

Pequeña migración 1

Intentemos combinar estos dos cambios de esquema:

  • Cambio de nombre de columna.
  • Añadir una nueva columna.

Primero, haga los cambios en la Película modelo:

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)

Luego genera la migración:

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

Eche un vistazo al código fuente de migración generado en _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)

Esta vez el código fuente de migración es correcto. Está cambiando el nombre del ReleaseYear columna a YearOfRelease y agregando el nuevo BoxOfficeRevenue columna.

Pequeña migración 2

El cambio de esquema restante que tenemos que hacer es Soltar una columna.

Aplicar este cambio en la Película modelo:

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)

Luego genera una nueva migración:

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

Eche un vistazo al código fuente de migración generado _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)

Esto es correcto. Está descartando los RuntimeMinutes columna.

Aplicar las dos migraciones pendientes

Las dos pequeñas migraciones fueron creadas y verificadas para su corrección. Ambos son migraciones pendientes.

Echa un vistazo a la lista de migraciones:

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)

Ahora aplica las dos migraciones pendientes:

dotnet ef database update
Code language: PowerShell (powershell)

Tenga en cuenta que esto aplicó ambas migraciones

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

Debido a que los cambios de esquema se realizaron por separado, las migraciones generadas fueron correctas y no hubo pérdida de datos.

¿Qué sucede cuando cambias una tabla que tiene datos?

Hay muchos cambios de esquema diferentes que puede hacer. La mayoría de ellos se pueden aplicar a tablas con datos existentes sin problemas. Sin embargo, no se arriesgue:pruebe siempre sus migraciones en un entorno de desarrollo con tablas que contengan datos.

Dicho esto, hay algunos cambios de esquema que no se aplicarán a una tabla con datos en ellos. Cuando se encuentre con esta situación, es posible que pueda personalizar la migración para resolver el problema. Mostraré dos ejemplos de esto a continuación.

Cambiar una columna anulable a no anulable

Cuando intenta cambiar una columna anulable para que no permita nulos, y la tabla ya tiene valores NULL en esa columna, la migración generada no la manejará correctamente. Te encontrarás con este error:

Para resolver este problema, puede personalizar la migración actualizando los valores nulos a un valor predeterminado antes de que cambie la columna. A continuación, mostraré un ejemplo completo de este escenario.

Ejemplo de cómo hacer que un anulable no permita valores nulos

El Director la columna es actualmente anulable. Para que no admita valores NULL, agregue el atributo [Obligatorio]:

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)

Cree la migración para este cambio:

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

Eche un vistazo al código fuente de migración generado en _Database_v5.cs y personalícelo ejecutando una declaración de ACTUALIZACIÓN con migraciónBuilder.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)

Aplicar la migración:

dotnet ef database update
Code language: PowerShell (powershell)

Esto pudo aplicar la migración sin problemas, porque reemplazó todos los nulos con cadenas vacías y luego cambió la columna para no permitir nulos.

Reducir la longitud de una columna de cadena

Supongamos que tiene cadenas existentes de 50 caracteres y desea cambiar la longitud máxima de esta columna de cadena a 40 caracteres. Cuando intente aplicar este cambio de esquema, se encontrará con el siguiente error:

Primero, asegúrese de estar de acuerdo con truncar los datos existentes en la tabla.

Puede resolver este problema al personalizar la migración para truncar la columna de cadena antes de que cambie la columna.

Descargo de responsabilidad:Hacer esto resultará en la pérdida de datos porque estará truncando intencionalmente la columna de cadena. No hagas esto si no quieres perder datos.

Ejemplo de reducción de la longitud de una columna de cadena

Primero, cambie el atributo [MaxLength] para la Descripción columna:

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)

Luego crea una nueva migración:

dotnet ef migrations add Database_v6

Ahora eche un vistazo al código fuente de migración generado en _Database_v6.c s y personalícelo mediante la ejecución de una declaración de ACTUALIZACIÓN con migraciónBuilder.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)

Aplicar la migración:

dotnet ef database update
Code language: PowerShell (powershell)

Esto aplicó con éxito la migración al truncar primero la columna de cadena a la longitud reducida, luego cambió la longitud de la columna.