5

Ho un DB che ho creato utilizzando l'inizializzatore del database OOB e sto utilizzando Code First con EF 4.3.1.EF 4.3.1 Eccezione di migrazione - AlterColumn defaultValueSql crea lo stesso nome di vincolo predefinito per diverse tabelle

Volevo sfruttare il nuovo flag "IgnoreChanges" sul cmdlet Add-Migration, in modo da poter modificare alcune delle mie colonne e aggiungere un valore SQL predefinito. In sostanza, alcune delle mie entità hanno una colonna denominata DateLastUpdated, che mi piacerebbe impostare il DEFAULT per l'espressione sql GETDATE().

ho creato l'InitialMigration con "Add-migrazione -ignorechanges InitialMigration", e poi ho aggiunto il testo seguente Su() e Giù():

public override void Up() 
{ 
    AlterColumn("CustomerLocations", "DateLastUpdated", c => c.DateTime(defaultValueSql: "GETDATE()")); 
    AlterColumn("UserReportTemplates", "DateLastUpdated", c => c.DateTime(defaultValueSql: "GETDATE()")); 
    AlterColumn("Chains", "DateLastUpdated", c => c.DateTime(defaultValueSql: "GETDATE()")); 
} 

public override void Down() 
{ 
    AlterColumn("CustomerLocations", "DateLastUpdated", c => c.DateTime()); 
    AlterColumn("UserReportTemplates", "DateLastUpdated", c => c.DateTime()); 
    AlterColumn("Chains", "DateLastUpdated", c => c.DateTime()); 
} 

Poi ho provato a fare funzionare "Update-Database - verbose", ma vedo che si sta cercando di creare lo stesso vincolo predefinito di nome sul database, e SQL genera un'eccezione:

Applying explicit migrations: [201203221856095_InitialMigration]. 
Applying explicit migration: 201203221856095_InitialMigration. 
ALTER TABLE [CustomerLocations] ADD CONSTRAINT DF_DateLastUpdated DEFAULT GETDATE() FOR [DateLastUpdated] 
ALTER TABLE [CustomerLocations] ALTER COLUMN [DateLastUpdated] [datetime] 
ALTER TABLE [UserReportTemplates] ADD CONSTRAINT DF_DateLastUpdated DEFAULT GETDATE() FOR [DateLastUpdated] 
System.Data.SqlClient.SqlException (0x80131904): There is already an object named 'DF_DateLastUpdated' in the database. 
Could not create constraint. See previous errors. 
    at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection) 
    at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection) 
    at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning() 
    at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj) 
    at System.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean async) 
    at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe) 
    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) 
    at System.Data.Entity.Migrations.DbMigrator.ApplyMigration(DbMigration migration, DbMigration lastMigration) 
    at System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.ApplyMigration(DbMigration migration, DbMigration lastMigration) 
    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() 
There is already an object named 'DF_DateLastUpdated' in the database. 
Could not create constraint. See previous errors. 

sembra EF sta creando il vincolo DEFAULT aggiungendo 'DF_' con il nome della colonna, ma non usando il nome della tabella, per fare questo unico al tavolo. È un bug noto, o sto facendo qualcosa di sbagliato qui?

+0

controllo Elyas risposta. è una bella soluzione – Morteza

risposta

7

Sembra si tratta di un bug noto: msdn forums

Andrew J Peters Microsoft (MSFT) ha risposto:

Grazie per aver segnalato questo. Il problema verrà risolto per RTM.

Una possibile soluzione consiste nel rendere inizialmente nullable la colonna, che impedisce a Migrazioni di generare il vincolo DEFAULT in più. Una volta creata la colonna, può essere modificata di nuovo su non annullabile.

Ma non è stato risolto in EF 4.3.1. Ecco parte rilevante della fonte:

// Type: System.Data.Entity.Migrations.Sql.SqlServerMigrationSqlGenerator 
// Assembly: EntityFramework, Version=4.3.1.0, 
// Culture=neutral, PublicKeyToken=b77a5c561934e089 
namespace System.Data.Entity.Migrations.Sql 
{ 
    public class SqlServerMigrationSqlGenerator : MigrationSqlGenerator 
    { 
    protected virtual void Generate(AlterColumnOperation alterColumnOperation) 
    { 
     //... 
     writer.Write("ALTER TABLE "); 
     writer.Write(this.Name(alterColumnOperation.Table)); 
     writer.Write(" ADD CONSTRAINT DF_"); 
     writer.Write(column.Name); 
     writer.Write(" DEFAULT "); 
     //... 

Quindi EF non tenta di rendere univoco il nome del vincolo.

Si dovrebbe provare la soluzione alternativa e segnalarlo come un bug.

EDIT: Ho appena realizzato che sopra menzionato Generate metodo è virtual così nel peggiore dei casi si può ereditare da SqlServerMigrationSqlGenerator e fissare la generazione SQL e impostarlo come il generatore di sql in Configuration.cs:

public Configuration() 
{ 
    AutomaticMigrationsEnabled = true; 
    SetSqlGenerator("System.Data.SqlClient", 
     new MyFixedSqlServerMigrationSqlGenerator()); 
} 

EDIT 2:

penso che la cosa migliore da fare fino a quando non fissato a ripiegare a SQL grezzo:

public override void Up() 
{ 
    Sql(@"ALTER TABLE [CustomerLocations] ADD CONSTRAINT 
     DF_CustomerLocations_DateLastUpdated 
     DEFAULT GETDATE() FOR [DateLastUpdated]"); 
    Sql(@"ALTER TABLE [CustomerLocations] ALTER COLUMN 
     [DateLastUpdated] [datetime]"); 
    //... 
} 
+0

dove hai preso il codice per EF? Sto usando Reflector, ma una parte del metodo Generate probabilmente usa un sacco di lambdas, funcs/actions e roba anonima, quindi Reflector non me ne dà una buona versione da copiare e modificare. –

+0

Ho usato il decompilatore incorporato in Resharper (puoi usare il decompilatore Jetbrains gratuito [dotPeek] (http://www.jetbrains.com/decompiler/index.html?topDP)) e anche JustDecompile di Telerik genera quasi lo stesso fonte .. ma entrambi contengono un sacco di lambad :(quindi non è riutilizzabile direttamente – nemesv

+0

@ThiagoSilva Penso che la cosa più semplice da fare sia scrivere questo tipo di migrazione in SQL raw fino a quando questo errore non sia stato corretto.Io ho aggiornato la mia risposta – nemesv

7

Ok, in base alla risposta del nemesv (accettato), ecco come ho finito per risolvere il problema, per ora, fino a che una correzione è ufficialmente rilasciato:

internal class MyFixedSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator 
{ 
    protected override void Generate(AlterColumnOperation alterColumnOperation) 
    { 
     if (alterColumnOperation == null) 
      throw new ApplicationException("alterColumnOperation != null"); 

     ColumnModel column = alterColumnOperation.Column; 
     if ((column.DefaultValue != null) || !string.IsNullOrWhiteSpace(column.DefaultValueSql)) 
     { 
      using (IndentedTextWriter writer = Writer()) 
      { 
       writer.Write("ALTER TABLE "); 
       writer.Write(this.Name(alterColumnOperation.Table)); 
       writer.Write(" ADD CONSTRAINT DF_"); 
       writer.Write(alterColumnOperation.Table + "_"); // <== THIS IS THE LINE THAT FIXES THE PROBLEM 
       writer.Write(column.Name); 
       writer.Write(" DEFAULT "); 
       writer.Write(column.DefaultValue != null ? base.Generate(column.DefaultValue) : column.DefaultValueSql); 
       writer.Write(" FOR "); 
       writer.Write(this.Quote(column.Name)); 
       this.Statement(writer); 
      } 
     } 
     using (IndentedTextWriter writer2 = Writer()) 
     { 
      writer2.Write("ALTER TABLE "); 
      writer2.Write(this.Name(alterColumnOperation.Table)); 
      writer2.Write(" ALTER COLUMN "); 
      writer2.Write(this.Quote(column.Name)); 
      writer2.Write(" "); 
      writer2.Write(this.BuildColumnType(column)); 
      if (column.IsNullable.HasValue && !column.IsNullable.Value) 
      { 
       writer2.Write(" NOT NULL"); 
      } 
      this.Statement(writer2); 
     } 
    } 
} 


internal sealed class Configuration : DbMigrationsConfiguration<MyDbContext> 
{ 
    public Configuration() 
    { 
     AutomaticMigrationsEnabled = true; 

     SetSqlGenerator("System.Data.SqlClient", new MyFixedSqlServerMigrationSqlGenerator()); 
    } 
    ... 
} 
+1

Ho eseguito in un problema in cui 'alterColumnOperation.Table' includeva lo schema, risultando in un nome non valido per il vincolo (come 'DF_dbo.MyTable_MyColumn'). con questo metodo di supporto: 'stringa privata RemoveSchema (nome stringa) {return name.Split ('.'). Last(); } –

+0

non so se è più sicuro, ma nel caso in cui cambino il metodo EF predefinito, invece del secondo "using IndentedTextWriter ...", puoi impostare i valori predefiniti su "null" e lasciare che il metodo di base terminare la creazione della clausola: 'column.DefaultValue = null; column.DefaultValueSql = null; base.Generate (alterColumnOperation); ' – drzaus

+1

Ancora non risolto in EF 6.0.0.3. Questo è piuttosto zoppo ... – Liviu

1

Questa soluzione testata in EF 6.1.3. molto probabilmente funziona su versioni precedenti.

è possibile implementare una classe generatore di SQL personalizzata derivata dai SqlServerMigrationSqlGenerator da System.Data.Entity.SqlServer namespace:

using System.Data.Entity.Migrations.Model; 
using System.Data.Entity.SqlServer; 

namespace System.Data.Entity.Migrations.Sql{ 
    internal class FixedSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator { 
     protected override void Generate(AlterColumnOperation alterColumnOperation){ 
      ColumnModel column = alterColumnOperation.Column; 
      var sql = String.Format(@"DECLARE @ConstraintName varchar(1000); 
      DECLARE @sql varchar(1000); 
      SELECT @ConstraintName = name FROM sys.default_constraints 
       WHERE parent_object_id = object_id('{0}') 
       AND col_name(parent_object_id, parent_column_id) = '{1}'; 
      IF(@ConstraintName is NOT Null) 
       BEGIN 
       set @sql='ALTER TABLE {0} DROP CONSTRAINT [' + @ConstraintName+ ']'; 
      exec(@sql); 
      END", alterColumnOperation.Table, column.Name); 
       this.Statement(sql); 
      base.Generate(alterColumnOperation); 
      return; 
     } 
     protected override void Generate(DropColumnOperation dropColumnOperation){ 
      var sql = String.Format(@"DECLARE @SQL varchar(1000) 
       SET @SQL='ALTER TABLE {0} DROP CONSTRAINT [' + (SELECT name 
        FROM sys.default_constraints 
        WHERE parent_object_id = object_id('{0}') 
        AND col_name(parent_object_id, parent_column_id) = '{1}') + ']'; 
      PRINT @SQL; 
       EXEC(@SQL); ", dropColumnOperation.Table, dropColumnOperation.Name); 

        this.Statement(sql); 
      base.Generate(dropColumnOperation); 
     } 
    } 
} 

e impostare questa configurazione:

internal sealed class Configuration : DbMigrationsConfiguration<MyDbContext> 
{ 
    public Configuration() 
    { 
     AutomaticMigrationsEnabled = true; 

     SetSqlGenerator("System.Data.SqlClient", new FixedSqlServerMigrationSqlGenerator()); 
    } 
    ... 
}