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

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

无法将值 NULL 插入表“ Director”列中 ‘ MOVIES _ cf7bad808fa94f89afa2e5dae1161e78.dbo.MOVIES’; 列不能 允许空值。 UPDATE 失败。语句已终止。

这是由于一些记录在其 Director列中具有 NULL。如何自动将这些值更改为某个默认的(比如“ JohnDoe”)控制器?

这是我的模型:

  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());
}
}
95713 次浏览

If I remember correctly, something like this should work:

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

Note: The defaultValueSql parameter value is treated as a verbatim SQL statement, so if the required value is effectively a string, like the John Doe example, then single quotes are required around the value.

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

In addition to the answer from @webdeveloper and @Pushpendra, you need to manually add updates to your migration to update existing rows. For example:

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

This is because AlterColumn produces DDL to set the default of the column to some specific value in the table specification. The DDL does not affect existing rows in the database.

You're actually making two changes at the same time (setting the default and making the column NOT NULL) and each of them is valid individually, but since you're making the two at the same time, you can expect the system to 'intelligently' realize your intent and set all NULL values to the default value, but this is not what's expected all the time.

Suppose you're only setting the default value for the column, and not making it NOT NULL. You obviously don't expect all the NULL records to be updated with the default you provide.

So, in my opinion, this is not a bug, and I don't want EF to update my data in the ways that I don't explicitly tell it to do. The developer is responsible to instruct the system about what to do with the data.

not sure if this option was always around but just ran into a similar issue, found that i was able to set the default value without running any manual updates using the following

defaultValueSql: "'NY'"

I got an error when the value provided was "NY" then i realized that they are expecting a SQL value like "GETDATE()" so i tried "'NY'" and that did the trick

the entire line looks like this

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

Thanks to this answer, got me on the right track

I found that just using Auto-Property Initializer on entity property is enough to get the job done.

For example:

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

For some reason, that I was unable to explain myself the approved answer does no longer works for me.

It worked on another app, on the one that I am working it doesn't.

So, an alternative, but quite inefficient, solution would be to override the SaveChanges() Method as shown bellow. This method should be on the Context class.

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

Many of the other responses focus on how to manually intervene when these issues occur.

After generating the Migration perform either of the following changes to the migration:

  1. Modify the Column definition to include a defaultValue or defaultSql statement:
    AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));

  2. Inject a SQL statement to pre-fill the existing columns, before the AlterColumn:
    Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");

Keep in mind that manual changes applied to a migration script will be overwritten if you re-scaffold the migration. For the first solution, it is pretty easy to extend EF to define a default value on a field automatically as part of the migration generation.

NOTE: EF doesn't automatically do this for you because the default value implementation would be different for each RDBMS provider, but also because default values have less meaning in a pure EF runtime because each row insert will provide the current value for each property, even if it is null, so the default value constraint never gets evaluated.
This AlterColumn statement is the only time that the default constraint comes into play, I guess this become a lower priority for the team that designed the SQL Server Migration Implementation.

The following solution combines attribute notation, model configuration conventions, and column annotations to pass through metadata to a custom Migration code generator. Steps 1 and 2 can be replaced with fluent notation for each affected field if you are not using attribute notation.
There are a lot of techniques in play here, feel free to use some or all, I hope that there is value for everyone here


  1. Declare the Default Value
    Create or re-purpose an existing attribute to define the default value to use, for this example we will create a new attribute called DefaultValue that inherits from ComponentModel.DefaultValueAttribute, as the usage is intuitive and there is a chance that existing code bases already implement this attribute. With this implementation you only need to use this specific attribute to access DefaultValueSql which is useful for dates and other custom scenarios.

    Implementation

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

    Attrribute Definition

    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. Create a convention to inject the default value into the column annotations
    Column annotations are used to used to pass custom metadata about columns through to the migration script generator.
    Using a convention to do this demonstrates the power behind Attribute notation to simplify how fluent metadata can be defined and manipulated for many properties rather than specifying it individually for each field.

    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. Add the Convention to the DbContext
    There are many ways to acheive this, I like to declare the conventions as the first custom step in my ModelCreation logic, this will be in your DbContext class.

    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. Override the MigrationCodeGenerator
    Now that those annotations have been applied to the column definitions within the model, we need to modify the migration script generator to use those annotations. For this we will inherit from the System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator as we only need to inject a minimal amount of change.
    Once we have processed our custom annotation, we need to remove it from the column definition to prevent it being serialised to the final output.

    See the base class code to explore other usage: 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. Register the CustomCodeGenerator
    Last step, in the DbMigration Configuration file we need to specify the Code Generator to use, look for Configuration.cs in your Migration folder by default...

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

Since EF Core 2.1, you can use MigrationBuilder.UpdateData to change values before altering the column (cleaner than using raw 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);
}