简体   繁体   中英

Entity framework core migration: Change column from non-nullable to nullable then set values to null

I decided to change a column from non-nullable to a nullable one. No issue here. I changed the property to have "?" in the type ( DateTime => DateTime? ) and the migration generated is OK.

Though, I want to change the value considered as "null" to a real null. Meaning, changing the value 0001-01-01 00:00:00 to null .

I created an extension method that properly generated the SQL code for up and down. I executed this after the column change in Up and before in Down .

The issue comes when executing the Up because the SQL from my extension method is executed before the changes of the column alteration is applied.

Is there anyway to force changes to be apply before executing this statement? (See FIXME in the code where the code could be inserted. Two locations possible.)

Extension class

public static class MigrationsCorrections
{
    public static OperationBuilder<SqlOperation> ChangeDateToNullable(this MigrationBuilder migrationBuilder, string table, string column)
    {
        //FIXME: Either having a call here this to force previous changes to be applied
        return migrationBuilder.Sql($"UPDATE {table} SET {column} = null WHERE {column} = '0001-01-01 00:00:00';");
    }

    public static OperationBuilder<SqlOperation> ChangeDateToNotNullable(this MigrationBuilder migrationBuilder, string table, string column)
    {
        return migrationBuilder.Sql($"UPDATE {table} SET {column} = '0001-01-01 00:00:00' WHERE {column} = null;");
    }
}

Migration class

// IMPORTATION OF THE EXTENSION CLASS
public partial class MetaPersonBirthDateNullable : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<DateTime>(
            name: "BirthDay",
            table: "MetaPersons",
            type: "TEXT",
            nullable: true,
            oldClrType: typeof(DateTime),
            oldType: "TEXT");
        //FIXME: Either having a call here this to force previous changes to be applied
        migrationBuilder.ChangeDateToNullable("MetaPersons", "BirthDay");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.ChangeDateToNotNullable("MetaPersons", "BirthDay");
        migrationBuilder.AlterColumn<DateTime>(
            name: "BirthDay",
            table: "MetaPersons",
            type: "TEXT",
            nullable: false,
            defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
            oldClrType: typeof(DateTime),
            oldType: "TEXT",
            oldNullable: true);
    }
}

SQL generated by migration

BEGIN TRANSACTION;

UPDATE MetaPersons SET BirthDay = null WHERE BirthDay = '0001-01-01 00:00:00';

CREATE TABLE "ef_temp_MetaPersons" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_MetaPersons" PRIMARY KEY AUTOINCREMENT,
    "BirthDay" TEXT NULL,
    "DeathDay" TEXT NULL,
    "ExternalId" TEXT NULL,
    "MetaSource" TEXT NULL,
    "Name" TEXT COLLATE NOCASE NULL,
    "Professions" TEXT NULL
);

INSERT INTO "ef_temp_MetaPersons" ("Id", "BirthDay", "DeathDay", "ExternalId", "MetaSource", "Name", "Professions")
SELECT "Id", "BirthDay", "DeathDay", "ExternalId", "MetaSource", "Name", "Professions"
FROM "MetaPersons";

COMMIT;

PRAGMA foreign_keys = 0;

BEGIN TRANSACTION;

DROP TABLE "MetaPersons";

ALTER TABLE "ef_temp_MetaPersons" RENAME TO "MetaPersons";

COMMIT;

PRAGMA foreign_keys = 1;

BEGIN TRANSACTION;

CREATE UNIQUE INDEX "IX_MetaPersons_ExternalId_MetaSource" ON "MetaPersons" ("ExternalId", "MetaSource");

CREATE INDEX "IX_MetaPersons_Name" ON "MetaPersons" ("Name");

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20220309145323_MetaPerson.BirthDate Nullable', '6.0.2');

COMMIT;

As we can see, the UPDATE statement is not in the proper order

EDIT 1

I tried using a MigrationOperation class instead as per Microsoft Custom Migration , but I get...

An operation of type 'ChangeDateToNullable' will be attempted while a rebuild of table 'MetaPersons' is pending. The database may not be in an expected state. Review the SQL generated by this migration to help diagnose any failures. Consider moving these operations to a subsequent migration.

This particular solution is one I thought, having a second migration, but that's not what I would prefer as those changes are linked together.

From the documentation Using MigrationBuilder.Sql() :

Use the EXEC function when a statement must be the first or only one in a SQL batch. It might also be needed to work around parser errors in idempotent migration scripts that can occur when referenced columns don't currently exist on a table.

So better create two migrations, one for ALTER another for UPDATE.

I fixed the issue pursuing in the direction of MigrationOperations. I overwrite the Generate method of IMigrationsSqlGenerator .

For the sake of having a generalized solution, I created a new Database project that includes this new MigrationsSqlGenerator with the classes needed to change the date column value from/to null. I also added two classes to execute any SQL code that respect the order and thus enables changing the value of a column after altering it. And all this, in the same migration.

Seehttps://github.com/djon2003/com.cyberinternauts.csharp.Database for the complete solution.

And https://www.codeproject.com/Articles/5327089/Executing-SQL-code-within-EntityFrameworkCore-migr for the related article.

Final SQL code generated:

BEGIN TRANSACTION;

CREATE TABLE "ef_temp_MetaPersons" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_MetaPersons" PRIMARY KEY AUTOINCREMENT,
    "BirthDay" TEXT NULL,
    "DeathDay" TEXT NULL,
    "ExternalId" TEXT NULL,
    "MetaSource" TEXT NULL,
    "Name" TEXT COLLATE NOCASE NULL,
    "Professions" TEXT NULL
);

INSERT INTO "ef_temp_MetaPersons" ("Id", "BirthDay", "DeathDay", "ExternalId", "MetaSource", "Name", "Professions")
SELECT "Id", "BirthDay", "DeathDay", "ExternalId", "MetaSource", "Name", "Professions"
FROM "MetaPersons";

COMMIT;

PRAGMA foreign_keys = 0;

BEGIN TRANSACTION;

DROP TABLE "MetaPersons";

ALTER TABLE "ef_temp_MetaPersons" RENAME TO "MetaPersons";

COMMIT;

PRAGMA foreign_keys = 1;

BEGIN TRANSACTION;

CREATE UNIQUE INDEX "IX_MetaPersons_ExternalId_MetaSource" ON "MetaPersons" ("ExternalId", "MetaSource");

CREATE INDEX "IX_MetaPersons_Name" ON "MetaPersons" ("Name");

UPDATE "MetaPersons" SET "BirthDay" = null WHERE "BirthDay" = '0001-01-01 00:00:00';

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20220309145323_MetaPerson.BirthDate Nullable', '6.0.2');

COMMIT;

As you can see, now the UPDATE statement is after the column alteration

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM