簡體   English   中英

實體框架遷移中必填字段的默認值?

[英]Default value for Required fields in Entity Framework migrations?

我已將[Required]數據注釋添加到ASP.NET MVC 應用程序中的模型之一。 創建遷移后,運行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());
    }
}

除了@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 不會影響數據庫中的現有行。

您實際上是在同時進行兩次更改(設置默認值並使列非空)並且每個更改都單獨有效,但是由於您同時進行了兩次更改,因此您可以期望系統智能地'意識到您的意圖並將所有NULL值設置為默認值,但這並不是一直預期的。

假設您只是為該列設置默認值,而不是將其設為 NOT NULL。 您顯然不希望使用您提供的默認值更新所有 NULL 記錄。

所以,在我看來,這不是一個錯誤,我不希望 EF 以我沒有明確告訴它的方式更新我的數據。 開發人員負責指示系統如何處理數據。

如果我沒記錯的話,這樣的事情應該有效:

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

注意:defaultValueSql 參數值被視為逐字 SQL 語句,因此如果所需的值實際上是一個字符串,如 John Doe 示例,則該值需要單引號。

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

不確定此選項是否始終存在,但只是遇到了類似的問題,發現我能夠使用以下方法設置默認值而無需運行任何手動更新

defaultValueSql: "'NY'"

當提供的值是"NY"時我收到一個錯誤然后我意識到他們期待像"GETDATE()"這樣的 SQL 值所以我嘗試了"'NY'"並且成功了

整條線看起來像這樣

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

感謝這個答案,讓我走上了正確的軌道

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

我發現僅在實體屬性上使用 Auto-Property Initializer 就足以完成工作。

例如:

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

許多其他響應集中在發生這些問題時如何手動干預。

生成遷移后,對遷移執行以下任一更改:

  1. 修改 Column 定義以包含 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 運行時中的意義較小,因為每行插入將為每個屬性提供當前值,即使它為空,所以默認值約束永遠不會被評估。
這個 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 US English culture. /// </param> public DefaultValueAttribute(Type type, string value) : base(value) { } } }
  2. 創建一個約定將默認值注入到列注釋中
    列注釋用於將有關列的自定義元數據傳遞給遷移腳本生成器。
    使用約定來執行此操作展示了 Attribute 符號背后的強大功能,可以簡化如何為許多屬性定義和操作流暢的元數據,而不是為每個字段單獨指定它。

     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 配置文件中,我們需要指定要使用的代碼生成器,默認情況下在您的遷移文件夾中查找 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 } }

出於某種原因,我無法解釋自己,批准的答案不再適合我。

它適用於另一個應用程序,而我正在使用的應用程序卻沒有。

因此,另一種但效率很低的解決方案是覆蓋 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";
            }
        }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM