[英]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;
}
許多其他響應集中在發生這些問題時如何手動干預。
生成遷移后,對遷移執行以下任一更改:
修改 Column 定義以包含 defaultValue 或 defaultSql 語句:
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));
在 AlterColumn 之前注入 SQL 語句以預填充現有列:
Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");
請記住,如果您重新構建遷移,則應用於遷移腳本的手動更改將被覆蓋。 對於第一個解決方案,很容易擴展 EF 以自動在字段上定義默認值,作為遷移生成的一部分。
注意:EF 不會自動為您執行此操作,因為每個 RDBMS 提供程序的默認值實現會有所不同,而且還因為默認值在純 EF 運行時中的意義較小,因為每行插入將為每個屬性提供當前值,即使它為空,所以默認值約束永遠不會被評估。
這個 AlterColumn 語句是默認約束發揮作用的唯一一次,我猜這成為設計 SQL Server 遷移實現的團隊的較低優先級。
以下解決方案結合了屬性表示法、模型配置約定和列注釋,以將元數據傳遞給自定義遷移代碼生成器。 如果您不使用屬性表示法,步驟 1 和 2 可以替換為每個受影響字段的流利表示法。
這里有很多技巧在玩,隨意使用一些或全部,希望這里對每個人都有價值
聲明默認值
創建或重新利用現有屬性來定義要使用的默認值,在本例中,我們將創建一個名為 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) { } } }
創建一個約定將默認值注入到列注釋中
列注釋用於將有關列的自定義元數據傳遞給遷移腳本生成器。
使用約定來執行此操作展示了 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; } } }
將約定添加到 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>(); }
覆蓋 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); } } }
注冊 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.