Valor padrão para campos obrigatórios em migrações do Entity Framework?


91

Eu adicionei a [Required]anotação de dados a um dos meus modelos em um aplicativo ASP.NET MVC . Depois de criar uma migração, a execução do Update-Databasecomando resulta no seguinte erro:

Não é possível inserir o valor NULL na coluna 'Diretor', tabela 'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies'; coluna não permite nulos. UPDATE falha. A instrução foi encerrada.

Isso se deve ao fato de alguns registros possuírem NULL em suas Directorcolunas. Como posso alterar automaticamente esses valores para algum diretor padrão (digamos "John Doe")?

Aqui está meu modelo:

  public class Movie
    {
        public int ID { get; set; }
        [Required]
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Required]
        public string Genre { get; set; }

        [Range(1,100)]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }

        [StringLength(5)]
        public string Rating { get; set; }

        [Required]     /// <--- NEW
        public string Director { get; set; }
    }

e aqui está minha última migração:

public partial class AddDataAnnotationsMig : DbMigration
{
    public override void Up()
    {
        AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false));
        AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false));
        AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
        AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false));
    }

    public override void Down()
    {
        AlterColumn("dbo.Movies", "Director", c => c.String());
        AlterColumn("dbo.Movies", "Rating", c => c.String());
        AlterColumn("dbo.Movies", "Genre", c => c.String());
        AlterColumn("dbo.Movies", "Title", c => c.String());
    }
}

Respostas:


74

Se bem me lembro, algo assim deve funcionar:

AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, defaultValueSql: "'John Doe'"));

Nota: O valor do parâmetro defaultValueSql é tratado como uma instrução SQL literal, portanto, se o valor necessário for efetivamente uma string, como o exemplo John Doe, as aspas simples serão necessárias em torno do valor.


9
Eu também pensei, mas isso não parece funcionar para os registros existentes. Ainda recebo um erro.
Andriy Drozdyuk,

@drozzy Talvez seja um bug, como aqui: EF 4.3.1 Exceção de migração - AlterColumn defaultValueSql cria o mesmo nome de restrição padrão para tabelas diferentes. Você pode atualizar linhas com IS NULLverificação por sua consulta.
webdeveloper

Interessante, mas não tenho certeza se entendi do que eles estão falando. No entanto, se for um bug, então sim, faria sentido.
Andriy Drozdyuk

6
Eu acho que deveria ser: "'John Doe'"- você precisa usar aspas SQL.
Sean,

1
@webdeveloper, não acho que seja um bug, por que AlterColumnatualizar os valores atuais? É um comando DDL (não DML).
Anton

110

Além da resposta de @webdeveloper e @Pushpendra, você precisa adicionar manualmente as atualizações à sua migração para atualizar as linhas existentes. Por exemplo:

public override void Up()
{
    Sql("UPDATE [dbo].[Movies] SET Title = 'No Title' WHERE Title IS NULL");
    AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
}

Isso ocorre porque AlterColumnproduz DDL para definir o padrão da coluna para algum valor específico na especificação da tabela. O DDL não afeta as linhas existentes no banco de dados.

Na verdade, você está fazendo duas alterações ao mesmo tempo (definindo o padrão e tornando a coluna NOT NULL) e cada uma delas é válida individualmente, mas como você está fazendo as duas ao mesmo tempo, pode esperar que o sistema ' de forma inteligente 'perceba sua intenção e defina todos NULL valores com o valor padrão, mas isso não é o que se espera o tempo todo.

Suponha que você esteja apenas definindo o valor padrão para a coluna, e não tornando-o NÃO NULO. Obviamente, você não espera que todos os registros NULL sejam atualizados com o padrão fornecido.

Então, na minha opinião, isso não é um bug, e eu não quero que a EF atualize meus dados da maneira que eu não digo explicitamente para fazer. O desenvolvedor é responsável por instruir o sistema sobre o que fazer com os dados.


17
Para as pessoas que encontram esta resposta no google: Acabei de tentar isso no EF6 e a instrução de atualização não parece ser necessária (mais). Acho que eles consideraram isso um bug, afinal.
EPLKleijntjens

3
Eu também posso garantir isso. Se você precisar de um valor padrão mesmo para um campo anulável, apenas altere-o primeiro para não anulável com um valor padrão e, em seguida, altere-o de volta para anulável. Muito útil para quando você adiciona um campo não anulável a uma classe filha :)
Wouter Schut

1
Explicação local. AlterColumn () apenas altera a definição da coluna. Não afeta os registros existentes de forma alguma
Korayem

10
public partial class AddDataAnnotationsMig : DbMigration
{
    public override void Up()
    {
        AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
        AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false,defaultValue:"Genre"));
        AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
        AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false,defaultValue:"Director"));

    }

    public override void Down()
    {       
        AlterColumn("dbo.Movies", "Director", c => c.String());
        AlterColumn("dbo.Movies", "Rating", c => c.String());
        AlterColumn("dbo.Movies", "Genre", c => c.String());
        AlterColumn("dbo.Movies", "Title", c => c.String());       
    }
}

2
Hum ... obrigado, mas como isso é diferente da resposta de @ webdeveloper?
Andriy Drozdyuk

1
não
informa

1
@Pushpendra, é engraçado como os desenvolvedores tendem a esquecer que uma vez, eles não sabiam muito. Gosto de respostas detalhadas que satisfaçam todos os níveis. Excelente trabalho!
útilBee

5

não tenho certeza se esta opção estava sempre disponível, mas apenas encontrei um problema semelhante, descobri que consegui definir o valor padrão sem executar nenhuma atualização manual usando o seguinte

defaultValueSql: "'NY'"

Recebi um erro quando o valor fornecido era, "NY"então percebi que eles estavam esperando um valor SQL como "GETDATE()"eu tentei "'NY'"e isso funcionou

toda a linha se parece com isso

AddColumn("TABLE_NAME", "State", c => c.String(maxLength: 2, nullable: false, defaultValueSql: "'NY'"));

Graças a esta resposta , me colocou no caminho certo


2

Desde EF Core 2.1, você pode usar MigrationBuilder.UpdateDatapara alterar os valores antes de alterar a coluna (mais limpo do que usar SQL bruto):

protected override void Up(MigrationBuilder migrationBuilder)
{
    // Change existing NULL values to NOT NULL values
    migrationBuilder.UpdateData(
        table: tableName,
        column: columnName,
        value: valueInsteadOfNull,
        keyColumn: columnName,
        keyValue: null);

    // Change column type to NOT NULL
    migrationBuilder.AlterColumn<ColumnType>(
        table: tableName,
        name: columnName,
        nullable: false,
        oldClrType: typeof(ColumnType),
        oldNullable: true);
}

1

Descobri que apenas usar o Auto-Property Initializer na propriedade da entidade é suficiente para fazer o trabalho.

Por exemplo:

public class Thing {
    public bool IsBigThing { get; set; } = false;
}

2
É uma boa resposta (me ajudou), mas isso não adiciona um valor padrão no banco de dados, ele define o valor no código.
chris31389

certo, ele não adicionou o valor padrão no banco de dados após mudanças de migração
Chetan Chaudhari

1

Muitas das outras respostas se concentram em como intervir manualmente quando esses problemas ocorrem.

Depois de gerar a migração, execute uma das seguintes alterações na migração:

  1. Modifique a definição da coluna para incluir uma instrução defaultValue ou defaultSql:
    AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));

  2. Injete uma instrução SQL para preencher previamente as colunas existentes, antes de AlterColumn:
    Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");

Lembre-se de que as alterações manuais aplicadas a um script de migração serão sobrescritas se você reorganizar a migração. Para a primeira solução, é muito fácil estender o EF para definir um valor padrão em um campo automaticamente como parte da geração da migração.

NOTA: EF não faz isso automaticamente para você porque a implementação do valor padrão seria diferente para cada provedor RDBMS, mas também porque os valores padrão têm menos significado em um tempo de execução de EF puro porque cada inserção de linha fornecerá o valor atual para cada propriedade, mesmo se for nulo, a restrição de valor padrão nunca é avaliada.
Esta instrução AlterColumn é a única vez em que a restrição padrão entra em ação, acho que isso se tornou uma prioridade menor para a equipe que projetou a implementação de migração do SQL Server.

A solução a seguir combina notação de atributo, convenções de configuração de modelo e anotações de coluna para passar metadados para um gerador de código de migração customizado. As etapas 1 e 2 podem ser substituídas por notação fluente para cada campo afetado se você não estiver usando notação de atributo.
Existem muitas técnicas em jogo aqui, fique à vontade para usar algumas ou todas, espero que haja valor para todos aqui


  1. Declare o valor padrão
    Crie ou reaproveite um atributo existente para definir o valor padrão a ser usado, para este exemplo, criaremos um novo atributo chamado DefaultValue que herda de ComponentModel.DefaultValueAttribute, pois o uso é intuitivo e existe uma chance de que existente as bases de código já implementam este atributo. Com esta implementação, você só precisa usar este atributo específico para acessar DefaultValueSql, que é útil para datas e outros cenários personalizados.

    Implementação

    [DefaultValue("Insert DefaultValue Here")]
    [Required]     /// <--- NEW
    public string Director { get; set; }
    
    // Example of default value sql
    [DefaultValue(DefaultValueSql: "GetDate()")]
    [Required]
    public string LastModified { get; set; }

    Definição de Atributo

    namespace EFExtensions
    {
        /// <summary>
        /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/>
        /// </summary>
        public class DefaultValueAttribute : System.ComponentModel.DefaultValueAttribute
        {
            /// <summary>
            /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/>
            /// </summary>
            public DefaultValueAttribute() : base("")
            {
            }
    
            /// <i
            /// <summary>
            /// Optional SQL to use to specify the default value.
            /// </summary>
            public string DefaultSql { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a Unicode character.
            /// </summary>
            /// <param name="value">
            /// A Unicode character that is the default value.
            /// </param>
            public DefaultValueAttribute(char value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using an 8-bit unsigned integer.
            /// </summary>
            /// <param name="value">
            /// An 8-bit unsigned integer that is the default value.
            /// </param>
            public DefaultValueAttribute(byte value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 16-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 16-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(short value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 32-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 32-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(int value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 64-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 64-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(long value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a single-precision floating point number.
            /// </summary>
            /// <param name="value">
            /// A single-precision floating point number that is the default value.
            /// </param>
            public DefaultValueAttribute(float value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a double-precision floating point number.
            /// </summary>
            /// <param name="value">
            /// A double-precision floating point number that is the default value.
            /// </param>
            public DefaultValueAttribute(double value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a System.Boolean value.
            /// </summary>
            /// <param name="value">
            /// A System.Boolean that is the default value.
            /// </param>
            public DefaultValueAttribute(bool value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a System.String.
            /// </summary>
            /// <param name="value">
            /// A System.String that is the default value.
            /// </param>
            public DefaultValueAttribute(string value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class.
            /// </summary>
            /// <param name="value">
            /// An System.Object that represents the default value.
            /// </param>
            public DefaultValueAttribute(object value) : base(value) { }
    
            /// /// <inheritdoc/>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class, converting the specified value to the specified type, and using an invariant
            /// culture as the translation context.
            /// </summary>
            /// <param name="type">
            /// A System.Type that represents the type to convert the value to.
            /// </param>
            /// <param name="value">
            /// A System.String that can be converted to the type using the System.ComponentModel.TypeConverter
            /// for the type and the U.S. English culture.
            /// </param>
            public DefaultValueAttribute(Type type, string value) : base(value) { }
        }
    }
  2. Crie uma convenção para injetar o valor padrão nas anotações da coluna As anotações da
    coluna são usadas para passar metadados personalizados sobre as colunas para o gerador de script de migração.
    Usar uma convenção para fazer isso demonstra o poder por trás da notação de atributo para simplificar como os metadados fluentes podem ser definidos e manipulados para muitas propriedades, em vez de especificá-los individualmente para cada campo.

    namespace EFExtensions
    {
    
        /// <summary>
        /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute
        /// </summary>
        public class DefaultValueConvention : Convention
        {
            /// <summary>
            /// Annotation Key to use for Default Values specified directly as an object
            /// </summary>
            public const string DirectValueAnnotationKey = "DefaultValue";
            /// <summary>
            /// Annotation Key to use for Default Values specified as SQL Strings
            /// </summary>
            public const string SqlValueAnnotationKey = "DefaultSql";
    
            /// <summary>
            /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute
            /// </summary>
            public DefaultValueConvention()
            {
                // Implement SO Default Value Attributes first
                this.Properties()
                        .Where(x => x.HasAttribute<EFExtensions.DefaultValueAttribute>())
                        .Configure(c => c.HasColumnAnnotation(
                            c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeKey(),
                            c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeValue()
                            ));
    
                // Implement Component Model Default Value Attributes, but only if it is not the SO implementation
                this.Properties()
                        .Where(x => x.HasAttribute<System.ComponentModel.DefaultValueAttribute>())
                        .Where(x => !x.HasAttribute<MiniTuber.DataAnnotations.DefaultValueAttribute>())
                        .Configure(c => c.HasColumnAnnotation(
                            DefaultValueConvention.DirectValueAnnotationKey, 
                            c.GetAttribute<System.ComponentModel.DefaultValueAttribute>().Value
                            ));
            }
        }
    
        /// <summary>
        /// Extension Methods to simplify the logic for building column annotations for Default Value processing
        /// </summary>
        public static partial class PropertyInfoAttributeExtensions
        {
            /// <summary>
            /// Wrapper to simplify the lookup for a specific attribute on a property info.
            /// </summary>
            /// <typeparam name="T">Type of attribute to lookup</typeparam>
            /// <param name="self">PropertyInfo to inspect</param>
            /// <returns>True if an attribute of the requested type exists</returns>
            public static bool HasAttribute<T>(this PropertyInfo self) where T : Attribute
            {
                return self.GetCustomAttributes(false).OfType<T>().Any();
            }
    
            /// <summary>
            /// Wrapper to return the first attribute of the specified type
            /// </summary>
            /// <typeparam name="T">Type of attribute to return</typeparam>
            /// <param name="self">PropertyInfo to inspect</param>
            /// <returns>First attribuite that matches the requested type</returns>
            public static T GetAttribute<T>(this System.Data.Entity.ModelConfiguration.Configuration.ConventionPrimitivePropertyConfiguration self) where T : Attribute
            {
                return self.ClrPropertyInfo.GetCustomAttributes(false).OfType<T>().First();
            }
    
            /// <summary>
            /// Helper to select the correct DefaultValue annotation key based on the attribute values
            /// </summary>
            /// <param name="self"></param>
            /// <returns></returns>
            public static string GetDefaultValueAttributeKey(this EFExtensions.DefaultValueAttribute self)
            {
                return String.IsNullOrWhiteSpace(self.DefaultSql) ? DefaultValueConvention.DirectValueAnnotationKey : DefaultValueConvention.SqlValueAnnotationKey;
            }
    
            /// <summary>
            /// Helper to select the correct attribute property to send as a DefaultValue annotation value
            /// </summary>
            /// <param name="self"></param>
            /// <returns></returns>
            public static object GetDefaultValueAttributeValue(this EFExtensions.DefaultValueAttribute self)
            {
                return String.IsNullOrWhiteSpace(self.DefaultSql) ? self.Value : self.DefaultSql;
            }
        }
    
    }
  3. Adicionar a convenção ao DbContext
    Há muitas maneiras de conseguir isso. Gosto de declarar as convenções como a primeira etapa personalizada em minha lógica ModelCreation, isso será em sua classe DbContext.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        // Use our new DefaultValueConvention
        modelBuilder.Conventions.Add<EFExtensions.DefaultValueConvention>();
    
        // My personal favourites ;)
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    
    }
  4. Substituir o MigrationCodeGenerator
    Agora que essas anotações foram aplicadas às definições de coluna dentro do modelo, precisamos modificar o gerador de script de migração para usar essas anotações. Para isso, herdaremos do System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator, pois só precisamos injetar uma quantidade mínima de mudança.
    Depois de processarmos nossa anotação personalizada, precisamos removê-la da definição de coluna para evitar que seja serializada para a saída final.

    Consulte o código da classe base para explorar outro uso: http://entityframework.codeplex.com/sourcecontrol/latest#src/EntityFramework/Migrations/Design/CSharpMigrationCodeGenerator.cs

    namespace EFExtensions
    {
        /// <summary>
        /// Implement DefaultValue constraint definition in Migration Scripts.
        /// </summary>
        /// <remarks>
        /// Original guide that provided inspiration for this https://romiller.com/2012/11/30/code-first-migrations-customizing-scaffolded-code/
        /// </remarks>
        public class CustomCodeGenerator : System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator
        {
            /// <summary>
            /// Inject Default values from the DefaultValue attribute, if the DefaultValueConvention has been enabled.
            /// </summary>
            /// <seealso cref="DefaultValueConvention"/>
            /// <param name="column"></param>
            /// <param name="writer"></param>
            /// <param name="emitName"></param>
            protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false)
            {
                var annotations = column.Annotations?.ToList();
                if (annotations != null && annotations.Any())
                {
                    for (int index = 0; index < annotations.Count; index ++)
                    {
                        var annotation = annotations[index];
                        bool handled = true;
    
                        try
                        {
                            switch (annotation.Key)
                            {
                                case DefaultValueConvention.SqlValueAnnotationKey:
                                    if (annotation.Value?.NewValue != null)
                                    {
                                        column.DefaultValueSql = $"{annotation.Value.NewValue}";
                                    }
                                    break;
                                case DefaultValueConvention.DirectValueAnnotationKey:
                                    if (annotation.Value?.NewValue != null)
                                    {
                                        column.DefaultValue = Convert.ChangeType(annotation.Value.NewValue, column.ClrType);
                                    }
                                    break;
                                default:
                                    handled = false;
                                    break;
                            }
                        }
                        catch(Exception ex)
                        {
                            // re-throw with specific debug information
                            throw new ApplicationException($"Failed to Implement Column Annotation for column: {column.Name} with key: {annotation.Key} and new value: {annotation.Value.NewValue}", ex);
                        }
    
                        if(handled)
                        {
                            // remove the annotation, it has been applied
                            column.Annotations.Remove(annotation.Key);
                        }
                    }
                }
                base.Generate(column, writer, emitName);
            }
    
            /// <summary>
            /// Generates class summary comments and default attributes
            /// </summary>
            /// <param name="writer"> Text writer to add the generated code to. </param>
            /// <param name="designer"> A value indicating if this class is being generated for a code-behind file. </param>
            protected override void WriteClassAttributes(IndentedTextWriter writer, bool designer)
            {
                writer.WriteLine("/// <summary>");
                writer.WriteLine("/// Definition of the Migration: {0}", this.ClassName);
                writer.WriteLine("/// </summary>");
                writer.WriteLine("/// <remarks>");
                writer.WriteLine("/// Generated Time: {0}", DateTime.Now);
                writer.WriteLine("/// Generated By: {0}", Environment.UserName);
                writer.WriteLine("/// </remarks>");
                base.WriteClassAttributes(writer, designer);
            }
    
    
        }
    }
  5. Registre o CustomCodeGenerator
    Última etapa, no arquivo de configuração DbMigration, precisamos especificar o gerador de código a ser usado, procure Configuration.cs em sua pasta de migração por padrão ...

    internal sealed class Configuration : DbMigrationsConfiguration<YourApplication.Database.Context>
    {
        public Configuration()
        {
            // I recommend that auto-migrations be disabled so that we control
            // the migrations explicitly 
            AutomaticMigrationsEnabled = false;
            CodeGenerator = new EFExtensions.CustomCodeGenerator();
        }
    
        protected override void Seed(YourApplication.Database.Context context)
        {
            //   Your custom seed logic here
        }
    }

0

Por alguma razão, não fui capaz de me explicar, a resposta aprovada não funciona mais para mim.

Funcionou em outro aplicativo, naquele em que estou trabalhando, não funciona.

Portanto, uma solução alternativa, mas bastante ineficiente , seria sobrescrever o método SaveChanges () conforme mostrado abaixo. Este método deve estar na classe Context.

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries().Where(entry => entry.Entity.GetType().GetProperty("ColumnName") != null))
        {
            if (entry.State == EntityState.Added)
            {
                entry.Property("ColumnName").CurrentValue = "DefaultValue";
            }
        }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.