实体框架迁移中必填字段的默认值?


91

我已经将[Required]数据注释添加到ASP.NET MVC应用程序中的其中一个模型中。创建迁移后,运行Update-Database命令将导致以下错误:

无法将值NULL插入表'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies'的“ Director”列中;列不允许为空。更新失败。该语句已终止。

这是由于某些记录的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());
    }
}

Answers:


74

如果我没记错的话,这样的事情应该起作用:

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

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


9
我也这么认为,但是对于现有记录来说似乎不起作用。所以我仍然得到一个错误。
Andriy Drozdyuk '09年

@drozzy也许是错误,例如这里:EF 4.3.1迁移异常-AlterColumn defaultValueSql为不同的表创建相同的默认约束名称您可以IS NULL通过查询查询来更新行。
webdeveloper 2012年

有趣,但是我不确定我了解他们在说什么。但是,如果这是一个错误,那就可以了。
Andriy Drozdyuk '09年

6
我认为应该是:"'John Doe'"-您需要使用SQL引号。
肖恩

1
@webdeveloper,我不认为这是一个错误,为什么要AlterColumn更新当前值?这是DDL(不是DML)命令。
安东

110

除了来自@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以我未明确告知它的方式更新数据。开发人员负责指导系统如何处理数据。


17
对于通过google找到此答案的人:我只是在EF6中尝试过此方法,而update语句似乎没有必要(不再)。我猜他们毕竟还是认为它是一个错误。
EPLKleijntjens

3
我也可以保证。如果即使对于可为空的字段也需要一个默认值,只需先将其更改为不可为空,然后再使用默认值,然后再将其更改回可为空。当您将非空字段添加到子类中时非常方便:)
Wouter Schut 2015年

1
当场解释。AlterColumn()仅更改列定义。它不会影响现有记录
Korayem '16

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
嗯...谢谢,但这和@webdeveloper的答案有什么不同?
Andriy Drozdyuk '09年

1
它没有告诉您必须在哪里添加默认值参数
Pushpendra 2012年

1
@Pushpendra,很有趣的是,开发人员往往会忘记他们曾经并不了解很多东西。我喜欢满足所有级别的详细答案。极好的工作!
有用的蜜蜂

5

不知道此选项是否一直存在,但是遇到了类似的问题,发现我可以使用以下命令设置默认值而无需运行任何手动更新

defaultValueSql: "'NY'"

当提供的值当时是错误的,"NY"我意识到他们期望像"GETDATE()"这样的SQL值,所以我尝试"'NY'"

整行看起来像这样

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

多亏了这个答案,我才能走上正轨


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

1

我发现仅对实体属性使用自动属性初始化程序就足以完成工作。

例如:

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

2
这是一个很好的答案(对我有所帮助),但这不会在数据库中添加默认值,而是在代码中设置该值。
chris313​​89

正确,它不会在迁移变更后在数据库中添加默认值
Chetan Chaudhari

1

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

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

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

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

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

注意:EF不会自动为您执行此操作,因为每个RDBMS提供程序的默认值实现会有所不同,而且因为在纯EF运行时中默认值的意义较小,因为每个行插入将为每个属性提供当前值,即使它为null,也不会评估默认值约束。
该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
        }
    }

0

由于某种原因,我无法向自己解释批准的答案对我而言不再有效。

它可以在另一款应用程序上工作,而在我正在使用的应用程序上却没有。

因此,另一种但效率不高的解决方案是,如下所示重写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";
            }
        }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.