EF5 Code First Migrations: “Column names in each t

2019-02-06 00:51发布

We're using Entity Framework 5.0 Code First and Automatic Migrations.

I had a class like so:

public class TraversalZones
{
    public int Low { get; set; }
    public int High { get; set; }
}​

Then we realized these properties weren't really the right names, so we changed them:

public class TraversalZones
{
    public int Left { get; set; }
    public int Top { get; set; }
}​

The rename refactored properly throughout the project, but I know Automatic Migrations aren't smart enough to pick up these explicit renames in the IDE, so I first checked to verify the only pending migration was this column rename:

update-database -f -script

Sure enough it just showed the SQL dropping Low and High and adding Left and Top. I then added a manual migration:

add-migration RenameColumns_TraversalZones_LowHigh_LeftTop

And fixed up the generated code to simply:

public override void Up()
{
    RenameColumn("TraversalZones", "Low", "Left");
    RenameColumn("TraversalZones", "High", "Top");
}

public override void Down()
{
    RenameColumn("TraversalZones", "Left", "Low");
    RenameColumn("TraversalZones", "Top", "High");
}

​ I then updated the db:

update-database -verbose

And got 2 column renames, just like I was expecting.

Several migrations later I backed up Production and Restored it to a local DB to test the code on this DB. This DB had the TraversalZones table already created in it, with the old column names (Low and High) I of course began by updating it:

update-database -f -verbose

And the rename commands appeared in the output - all appeared well:

EXECUTE sp_rename @objname = N'TraversalZones.Low', @newname = N'Left', @objtype = N'COLUMN'
EXECUTE sp_rename @objname = N'TraversalZones.High', @newname = N'Top', @objtype = N'COLUMN'
[Inserting migration history record]

I then ran my code, and it errored out telling me the database had changed since last run, and that I should run update-database... .

So I ran it again:

update-database -f -verbose

And am now stuck on this error:

No pending code-based migrations. Applying automatic migration:
201212191601545_AutomaticMigration.
ALTER TABLE [dbo].[TraversalZones] ADD [Left] [int] NOT NULL DEFAULT 0
System.Data.SqlClient.SqlException (0x80131904): Column names in each table must be unique. Column name 'Left' in table 'dbo.TraversalZones' is specified more than once.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at System.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean async, Int32 timeout)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at System.Data.Entity.Migrations.DbMigrator.ExecuteSql(DbTransaction transaction, MigrationStatement migrationStatement)
   at System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.ExecuteSql(DbTransaction transaction, MigrationStatement migrationStatement)
   at System.Data.Entity.Migrations.DbMigrator.ExecuteStatements(IEnumerable`1 migrationStatements)
   at System.Data.Entity.Migrations.Infrastructure.MigratorBase.ExecuteStatements(IEnumerable`1 migrationStatements)
   at System.Data.Entity.Migrations.DbMigrator.ExecuteOperations(String migrationId, XDocument targetModel, IEnumerable`1 operations, Boolean downgrading, Boolean auto)
   at System.Data.Entity.Migrations.DbMigrator.AutoMigrate(String migrationId, XDocument sourceModel, XDocument targetModel, Boolean downgrading)
   at System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.AutoMigrate(String migrationId, XDocument sourceModel, XDocument targetModel, Boolean downgrading)
   at System.Data.Entity.Migrations.DbMigrator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId)
   at System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId)
   at System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration)
   at System.Data.Entity.Migrations.Infrastructure.MigratorBase.Update(String targetMigration)
   at System.Data.Entity.Migrations.Design.ToolingFacade.UpdateRunner.RunCore()
   at System.Data.Entity.Migrations.Design.ToolingFacade.BaseRunner.Run()
ClientConnectionId:c40408ee-def3-4553-a9fb-195366a05fff
Column names in each table must be unique. Column name 'Left' in table 'dbo.TraversalZones' is specified more than once.​

So, clearly Migrations is confused as to whether the column "Left" still needs to make it into this table; I would assume RenameColumn would leave things in the proper state, but it appears it has not.

When I dump what it's attempting to do to a update-database -f -script, I get it trying to do exactly what it would have done if the manual migration were not there:

ALTER TABLE [dbo].[TraversalZones] ADD [Left] [int] NOT NULL DEFAULT 0
ALTER TABLE [dbo].[TraversalZones] ADD [Top] [int] NOT NULL DEFAULT 0
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'dbo.TraversalZones')
AND col_name(parent_object_id, parent_column_id) = 'Low';
IF @var0 IS NOT NULL
    EXECUTE('ALTER TABLE [dbo].[TraversalZones] DROP CONSTRAINT ' + @var0)
ALTER TABLE [dbo].[TraversalZones] DROP COLUMN [Low]
DECLARE @var1 nvarchar(128)
SELECT @var1 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'dbo.TraversalZones')
AND col_name(parent_object_id, parent_column_id) = 'High';
IF @var1 IS NOT NULL
    EXECUTE('ALTER TABLE [dbo].[TraversalZones] DROP CONSTRAINT ' + @var1)
ALTER TABLE [dbo].[TraversalZones] DROP COLUMN [High]
INSERT INTO [__MigrationHistory] ([MigrationId], [Model], [ProductVersion]) VALUES ('201212191639471_AutomaticMigration', 0x1F8B08000...000, '5.0.0.net40')

This appears to be a bug in Migrations.

1条回答
女痞
2楼-- · 2019-02-06 01:27

The workaround, obviously, is this:

update-database -f -script

Which you can see the results of in my question. Then I tossed everything from the script but the last line, and ran that against the DB to let Migrations know: We already renamed that column, cut it out.

I can now proceed with this copy of the database, but I'm concerned every migration against copies of Production (until Production itself has been migrated) will keep having this issue. How can I resolve this properly without this workaround?

Update

This was in fact an issue in every other instance including Production. The dirty solution was to generate a SQL script (update-database -f -script), after committing the generated version and the fixed version.

A slightly cleaner solution is to take the SQL from the script, add a manual migration, and change the contents of Up to simply:

public void Up()
{
    Sql("...That SQL you extracted from the script...");
}

This will ensure other environments running this migration do so precisely the way you intended.

Testing this is a bit tricky so you can approach it this way:

  1. Backup your db just in case.
  2. Run the SQL. If it works properly, set the SQL aside.
  3. Add the manual migration and wipe out everything in the Up() method. Leave it completely empty.
  4. Run update-database -f
  5. Now modify the Up() method by adding the Sql("..."); calling the SQL you set aside.

Now your db is up to date without running the SQL twice, and other environments get the results of that SQL.

查看更多
登录 后发表回答