【问题标题】:Default value for Required fields in Entity Framework migrations?实体框架迁移中必填字段的默认值?
【发布时间】:2012-09-09 20:05:18
【问题描述】:

我在ASP.NET MVC application 中的一个模型中添加了[Required] 数据注释。创建迁移后,运行Update-Database 命令会导致以下错误:

无法将值 NULL 插入到列 'Director' 中,表 'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies';列不 允许空值。更新失败。声明已终止。

这是由于某些记录在其Director 列中具有 NULL。如何自动将这些值更改为某个默认值(比如“John Doe”)导演?

这是我的模型:

  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; }
    }

这是我最新的迁移:

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());
    }
}

【问题讨论】:

    标签: c# asp.net-mvc entity-framework ef-code-first


    【解决方案1】:

    如果我没记错的话,这样的事情应该可以工作:

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

    注意:defaultValueSql 参数值被视为逐字 SQL 语句,因此如果所需的值实际上是一个字符串,如 John Doe 示例,则需要在值周围加上单引号。

    【讨论】:

    • 我也这么认为,但这似乎不适用于现有记录。所以我仍然得到一个错误。
    • @drozzy 也许是错误,就像这里:EF 4.3.1 Migration Exception - AlterColumn defaultValueSql creates same default constraint name for different tables 你可以用IS NULL 更新行,检查你的查询。
    • 很有趣,但我不确定我是否理解他们在说什么。但是,如果这是一个错误,那么是的,这是有道理的。
    • 我认为应该是:"'John Doe'" - 你需要使用 SQL 引号。
    • @webdeveloper,我不认为这是一个错误,为什么AlterColumn 会更新当前值?它是一个 DDL(不是 DML)命令。
    【解决方案2】:

    从 EF Core 2.1 开始,您可以在更改列之前使用 MigrationBuilder.UpdateData 更改值(比使用原始 SQL 更简洁):

    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);
    }
    

    【讨论】:

      【解决方案3】:

      许多其他响应都集中在如何在这些问题发生时进行手动干预。

      生成迁移后,对迁移执行以下任一更改:

      1. 修改 Column 定义以包含 defaultValue 或 defaultSql 语句:
        AlterColumn("dbo.Movies", "Director", c =&gt; c.String(nullable: false, default: ""));

      2. 在 AlterColumn 之前注入 SQL 语句以预填充现有列:
        Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");

      请记住,如果您重新构建迁移,则应用到迁移脚本的手动更改将被覆盖。 对于第一个解决方案,扩展 EF 以在迁移生成过程中自动定义字段的默认值非常容易。

      注意:EF 不会自动为您执行此操作,因为每个 RDBMS 提供程序的默认值实现会有所不同,还因为默认值在纯 EF 运行时中的意义较小,因为每个行插入都会提供当前值每个属性,即使它为空,所以永远不会评估默认值约束。
      这个 AlterColumn 语句是唯一一次使用默认约束,我想这对于设计 SQL Server 迁移实现的团队来说是一个较低的优先级。

      以下解决方案结合了属性表示法、模型配置约定和列注释,以将元数据传递到自定义迁移代码生成器。如果您不使用属性表示法,则可以将步骤 1 和 2 替换为每个受影响字段的流畅表示法。
      这里有很多技巧在玩,随意使用部分或全部,希望对大家有参考价值


      1. 声明默认值
        创建或重新定义现有属性以定义要使用的默认值,在本例中,我们将创建一个名为 DefaultValue 的新属性,该属性继承自 ComponentModel.DefaultValueAttribute,因为其用法很直观,并且现有代码库可能已经实现这个属性。有了这个实现,你只需要使用这个特定的属性来访问 DefaultValueSql,这对于日期和其他自定义场景很有用。

        实施

        [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; }
        

        属性定义

        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. 创建约定以将默认值注入列注释
        列注释用于将有关列的自定义元数据传递给迁移脚本生成器。
        使用约定来执行此操作展示了属性表示法背后的强大功能,可以简化如何为许多属性定义和操作流畅的元数据,而不是为每个字段单独指定它。

        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. 将约定添加到 DbContext
        有很多方法可以实现这一点,我喜欢将约定声明为我的 ModelCreation 逻辑中的第一个自定义步骤,这将在您的 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. 覆盖 MigrationCodeGenerator
        现在这些注释已应用于模型中的列定义,我们需要修改迁移脚本生成器以使用这些注释。为此,我们将从System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator 继承,因为我们只需要注入最少量的更改。
        一旦我们处理了我们的自定义注解,我们需要将它从列定义中移除,以防止它被序列化到最终输出。

        查看基类代码探索其他用法: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. 注册 CustomCodeGenerator
        最后一步,在 DbMigration 配置文件中我们需要指定要使用的代码生成器,默认在你的 Migration 文件夹中查找 Configuration.cs...

        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
            }
        }
        

      【讨论】:

        【解决方案4】:

        由于某种原因,我无法解释自己,批准的答案不再适合我。

        它适用于另一个应用程序,而我正在使用的应用程序却没有。

        因此,另一种效率很低的解决方案是覆盖 SaveChanges() 方法,如下所示。这个方法应该在 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";
                    }
                }
        

        【讨论】:

          【解决方案5】:

          我发现仅对实体属性使用 Auto-Property Initializer 就足以完成工作。

          例如:

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

          【讨论】:

          • 这是一个很好的答案(帮助了我),但这不会在数据库中添加默认值,它会在代码中设置值。
          • 对它没有在迁移更改后在数据库中添加默认值
          【解决方案6】:

          不确定这个选项是否一直存在,但遇到了类似的问题,发现我能够设置默认值,而无需使用以下方法运行任何手动更新

          defaultValueSql: "'NY'"

          当提供的值是 "NY" 时出现错误,然后我意识到他们期待像 "GETDATE()" 这样的 SQL 值,所以我尝试了 "'NY'" 并且成功了

          整行看起来像这样

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

          感谢this answer,让我走上正轨

          【讨论】:

            【解决方案7】:

            除了来自@webdeveloper 和@Pushpendra 的回答之外,您还需要手动将更新添加到迁移中以更新现有行。例如:

            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"));
            }
            

            这是因为AlterColumn 生成 DDL 以将列的默认值设置为表规范中的某个特定值。 DDL 不会影响数据库中的现有行。

            实际上,您同时进行了两项更改(设置默认值并使列 NOT NULL)并且每个更改都单独有效,但是由于您同时进行了两项更改,因此您可以期待系统“智能”实现您的意图并将所有 NULL 值设置为默认值,但这并不是一直以来所期望的。

            假设您只是设置列的默认值,而不是使其为 NOT NULL。您显然不希望使用您提供的默认值更新所有 NULL 记录。

            因此,在我看来,这不是错误,我不希望 EF 以我没有明确告诉它这样做的方式更新我的数据。开发人员负责指示系统如何处理数据。

            【讨论】:

            • 对于通过谷歌找到这个答案的人:我刚刚在 EF6 中尝试过这个,更新语句似乎没有必要(不再)。我猜他们毕竟认为这是一个错误。
            • 我也可以保证这一点。如果即使对于可为空的字段也需要默认值,只需先使用默认值将其更改为不可为空,然后再将其改回可为空。当您向子类添加不可为空的字段时非常方便:)
            • 现场解释。 AlterColumn() 只是改变列定义。它不会影响现有记录
            【解决方案8】:
            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());       
                }
            }
            

            【讨论】:

            • 嗯...谢谢,但这与@webdeveloper 的回答有何不同?
            • 它没有告诉你必须在哪里添加默认值参数
            • @Pushpendra,有趣的是,开发人员往往会忘记从前,他们并不了解太多。我喜欢满足所有级别的详细答案。干得好!
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2012-01-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-05-11
            相关资源
            最近更新 更多