实体框架6 Code First默认值

266

有没有一种“优雅”的方法为特定属性赋予默认值?

也许可以通过 DataAnnotations 来实现,类似于:

[DefaultValue("true")]
public bool Active { get; set; }

谢谢。


2
也许在构造函数中尝试 this.Active = true;? 我认为在获取时 DB 值将具有优先权,但是如果首先 new 然后附加实体进行更新而没有先获取,则要小心,因为更改跟踪 可能 会将其视为您想要更新该值的操作。 评论一下,因为我很久没有使用 EF 了,感觉这只是瞎猜。 - AaronLS
3
谢谢您的回复。到目前为止,我一直使用这种方法https://dev59.com/AG865IYBdhLWcg3wA5uc#5032578,但我认为可能有更好的方法。 - marino-krk
4
public bool Inactive { get; set; } - Jesse Hufstetler
正如微软文档所说:“您无法使用数据注释设置默认值。” - hmfarimani
问题太模糊了。请定义“优雅”或“更好的方式”。另外,默认值在哪里?在数据库中还是在对象中?无论哪种方式,对于EF来说,流畅的API就是你所需要的。 - undefined
显示剩余3条评论
19个回答

195

您可以通过手动编辑代码来执行第一个迁移操作:

public override void Up()
{    
   AddColumn("dbo.Events", "Active", c => c.Boolean(nullable: false, defaultValue: true));
} 

8
如果在创建一个“Event”对象时,OP没有特别将“Active”设置为“true”,我不确定这会起作用。对于非可空布尔属性,默认值始终为“false”,因此除非更改,否则实体框架将保存这个值到数据库中。或者我有所遗漏吗? - GFoley83
8
@GFoley83,是的,你说得对。这种方法只在数据库层面上添加默认约束。要实现完整的解决方案,您还需要在实体的构造函数中分配默认值或使用具有支持字段的属性,如上面的答案所示。 - gdbdable
1
这适用于基本类型。对于类似DATETIMEOFFSET的内容,请使用defaultValueSql: "SYSDATETIMEOFFSET",而不是defaultValue,因为defaultValue: System.DateTimeOffset.Now将解析为当前系统datetimeoffset值的字符串。 - OzBob
3
据我所知,如果重新搭建迁移环境,您的手动更改将会丢失。 - Alexander Powolozki
1
@ninbit 我认为你应该先编写迁移以在数据库级别上删除它,然后更改你的DAL映射。 - gdbdable
显示剩余3条评论

100

好久不见,为了其他人留个便条。 我使用一个属性实现了所需功能,并将该属性装饰到我的模型类字段上,以达到我想要的效果。

[SqlDefaultValue(DefaultValue = "getutcdate()")]
public DateTime CreatedDateUtc { get; set; }

得到这2篇文章的帮助:

我所做的:

定义属性

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SqlDefaultValueAttribute : Attribute
{
    public string DefaultValue { get; set; }
}
在上下文的"OnModelCreating"中。
modelBuilder.Conventions.Add( new AttributeToColumnAnnotationConvention<SqlDefaultValueAttribute, string>("SqlDefaultValue", (p, attributes) => attributes.Single().DefaultValue));

在自定义的SqlGenerator中

private void SetAnnotatedColumn(ColumnModel col)
{
    AnnotationValues values;
    if (col.Annotations.TryGetValue("SqlDefaultValue", out values))
    {
         col.DefaultValueSql = (string)values.NewValue;
    }
}

然后在迁移配置的构造函数中,注册自定义的 SQL 生成器。

SetSqlGenerator("System.Data.SqlClient", new CustomMigrationSqlGenerator());

6
您甚至可以在不给每个实体加上 [SqlDefaultValue(DefaultValue = "getutcdate()")] 的情况下全局执行此操作。 1)只需删除 modelBuilder.Conventions.Add(new AttributeToColumnAnnotationConvention<SqlDefaultValueAttribute, string>("SqlDefaultValue", (p, attributes) => attributes.Single().DefaultValue)); 2)添加 modelBuilder.Properties().Where(x => x.PropertyType == typeof(DateTime)).Configure(c => c.HasColumnType("datetime2").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed).HasColumnAnnotation("SqlDefaultValue", "getdate()")); - Daniel Skowroński
2
请问自定义的SqlGenerator在哪里? - ᴍᴀᴛᴛ ʙᴀᴋᴇʀ
3
自定义的 SqlGenerator 源自于此链接:https://andy.mehalick.com/2014/02/06/ef6-adding-a-created-datetime-column-automatically-with-code-first-migrations/。 - ravinsp
1
@ravinsp 为什么不自定义 MigrationCodeGenerator 类,以便迁移具有正确信息,而不是 SqlGenerator 代码?SQL 代码是最后一步... - Alex
@Alex 值得一试!这也可以工作,并且比注入 SQL 代码更优雅。但我不确定覆盖 C# MigrationCodeGenerator 的复杂性。 - ravinsp
显示剩余3条评论

96

以上回答确实有所帮助,但只提供了部分解决方案。 主要问题是,一旦删除默认值属性,数据库中列上的约束将不会被删除。因此,以前的默认值仍将保留在数据库中。

下面是解决问题的完整方案,包括删除属性后在 SQL 约束上的操作。 我还重新使用了 .NET Framework 的本机 DefaultValue 属性。

用法

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[DefaultValue("getutcdate()")]
public DateTime CreatedOn { get; set; }

为了使此操作生效,您需要更新 IdentityModels.csConfiguration.cs 文件。

IdentityModels.cs 文件

在您的 ApplicationDbContext 类中添加/更新此方法。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
            base.OnModelCreating(modelBuilder);
            var convention = new AttributeToColumnAnnotationConvention<DefaultValueAttribute, string>("SqlDefaultValue", (p, attributes) => attributes.SingleOrDefault().Value.ToString());
            modelBuilder.Conventions.Add(convention);
}

Configuration.cs文件

通过注册自定义的Sql生成器,更新您的Configuration类构造函数,如下所示:

internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
{
    public Configuration()
    {
        // DefaultValue Sql Generator
        SetSqlGenerator("System.Data.SqlClient", new DefaultValueSqlServerMigrationSqlGenerator());
    }
}

接下来,添加自定义 Sql 生成器类(可以将其添加到 Configuration.cs 文件或单独的文件中)

internal class DefaultValueSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    private int dropConstraintCount;

    protected override void Generate(AddColumnOperation addColumnOperation)
    {
        SetAnnotatedColumn(addColumnOperation.Column, addColumnOperation.Table);
        base.Generate(addColumnOperation);
    }

    protected override void Generate(AlterColumnOperation alterColumnOperation)
    {
        SetAnnotatedColumn(alterColumnOperation.Column, alterColumnOperation.Table);
        base.Generate(alterColumnOperation);
    }

    protected override void Generate(CreateTableOperation createTableOperation)
    {
        SetAnnotatedColumns(createTableOperation.Columns, createTableOperation.Name);
        base.Generate(createTableOperation);
    }

    protected override void Generate(AlterTableOperation alterTableOperation)
    {
        SetAnnotatedColumns(alterTableOperation.Columns, alterTableOperation.Name);
        base.Generate(alterTableOperation);
    }

    private void SetAnnotatedColumn(ColumnModel column, string tableName)
    {
        if (column.Annotations.TryGetValue("SqlDefaultValue", out var values))
        {
            if (values.NewValue == null)
            {
                column.DefaultValueSql = null;
                using var writer = Writer();

                // Drop Constraint
                writer.WriteLine(GetSqlDropConstraintQuery(tableName, column.Name));
                Statement(writer);
            }
            else
            {
                column.DefaultValueSql = (string)values.NewValue;
            }
        }
    }

    private void SetAnnotatedColumns(IEnumerable<ColumnModel> columns, string tableName)
    {
        foreach (var column in columns)
        {
            SetAnnotatedColumn(column, tableName);
        }
    }

    private string GetSqlDropConstraintQuery(string tableName, string columnName)
    {
        var tableNameSplitByDot = tableName.Split('.');
        var tableSchema = tableNameSplitByDot[0];
        var tablePureName = tableNameSplitByDot[1];

        var str = $@"DECLARE @var{dropConstraintCount} nvarchar(128)
SELECT @var{dropConstraintCount} = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'{tableSchema}.[{tablePureName}]')
AND col_name(parent_object_id, parent_column_id) = '{columnName}';
IF @var{dropConstraintCount} IS NOT NULL
EXECUTE('ALTER TABLE {tableSchema}.[{tablePureName}] DROP CONSTRAINT [' + @var{dropConstraintCount} + ']')";

        dropConstraintCount++;
        return str;
    }
}

2
这种方法对我来说非常完美。我做的一个增强是还覆盖了Generate(CreateTableOperation createTableOperation)和Generate(AddColumnOperation addColumnOperation),使用相同的逻辑,以便也能捕获这些情况。我只检查values.NewValue是否为空,因为我希望我的默认值是空字符串。 - Delorian
2
@Delorian,我已经更新了我的答案,感谢您的评论。 - J-M
1
我已经编辑了您的帖子,以支持回滚删除多个约束的情况。您的脚本会抛出一个错误,指出@con已经声明过了。我创建了一个私有变量来保存计数器,并简单地递增它。我还更改了删除约束的格式,使其更接近EF在创建约束时发送到SQL的格式。做得好! - Brad
1
谢谢提供的解决方案,但是我有两个问题:1. 表名需要加括号。2. 在更新时,新值没有被设置,而是默认值被设置了! - Omid.Hanjani
1
我需要设置[DatabaseGenerated(DatabaseGeneratedOption.Computed)]属性吗?如果需要,为什么?在我的测试中,似乎不设置它也没有影响。 - RamNow
显示剩余6条评论

30

你的模型属性不一定要是“自动属性”,尽管这样更容易。而DefaultValue属性实际上只是信息性元数据,答案在这里接受的是构造函数方法的一种替代方案。

public class Track
{

    private const int DEFAULT_LENGTH = 400;
    private int _length = DEFAULT_LENGTH;
    [DefaultValue(DEFAULT_LENGTH)]
    public int LengthInMeters {
        get { return _length; }
        set { _length = value; }
    }
}

对比。

public class Track
{
    public Track()
    {
        LengthInMeters = 400;   
    }

    public int LengthInMeters { get; set; }        
}

仅适用于使用此特定类创建和使用数据的应用程序。通常,如果数据访问代码是集中的,则这不是问题。要在所有应用程序中更新该值,您需要配置数据源以设置默认值。Devi的答案展示了如何使用迁移、SQL或任何语言来完成。


26
注意:这不会在数据库中设置默认值。其它没有使用你的实体的程序将无法得到该默认值。 - Eric J.
这部分的答案仅适用于通过Entity Framework插入记录的方式,如果您使用其他方式插入记录,则无法生效。此外,如果您在表中创建一个新的非空列,这也无法为现有记录设置默认值。@devi提供了有价值的补充信息。 - d512
为什么你的第一种方法是“正确”的方式?使用构造函数的方法会遇到意想不到的问题吗? - Ebsan
只是一种观点,而且那些会改变 :) 可能不会。 - calebboyd
2
在某些情况下,我使用构造函数的方法可能会出现问题;在后备字段中设置默认值似乎是最不侵入性的解决方案。 - Lucent Fox

16

我在实体的构造函数中初始化了值。

注意:DefaultValue属性不会自动设置属性的值,您必须自己设置。


6
构造函数设置数值的问题在于 EF 在提交事务时会对数据库进行更新。 - Luka
该模型首先通过这种方式创建默认值。 - Lucas
DefaultValue是一种生活在遥远的外国土地上的物种,它太害羞了,不敢直接接触你的属性。不要发出声音,它很容易被吓跑 - 如果它靠近到足以听到声音,那就更应该小心了。因为指出并非显而易见之点,所以加1分。 - Risadinha

10
我承认我的方法没有遵循完整的“Code First”概念。但是如果你有能力直接在表格本身中更改默认值...那么比起上述需要经历的复杂过程,它要简单得多...我只是太懒不想做那么多工作!

似乎原帖作者的想法几乎可以奏效:

[DefaultValue(true)]
public bool IsAdmin { get; set; }

我认为他们只是错误地添加了引号...但是很遗憾,这并没有那么直观。其他建议对我来说太过繁琐(尽管我有进入表格并进行更改所需的权限...并非每个开发人员在每种情况下都有这样的权限)。最终我只能用老方法来完成。我在SQL Server表中设置了默认值...真的,已经够了!注意:我进一步测试了add-migration和update-database,结果修改成功。enter image description here

9

在 @SedatKapanoglu 的评论之后,我添加了自己的方法,因为他是对的,仅使用流畅的API是不起作用的。

1- 创建自定义代码生成器并覆盖 ColumnModel 的 Generate 方法。

   public class ExtendedMigrationCodeGenerator : CSharpMigrationCodeGenerator
{

    protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false)
    {

        if (column.Annotations.Keys.Contains("Default"))
        {
            var value = Convert.ChangeType(column.Annotations["Default"].NewValue, column.ClrDefaultValue.GetType());
            column.DefaultValue = value;
        }


        base.Generate(column, writer, emitName);
    }

}

2- 分配新的代码生成器:

public sealed class Configuration : DbMigrationsConfiguration<Data.Context.EfSqlDbContext>
{
    public Configuration()
    {
        CodeGenerator = new ExtendedMigrationCodeGenerator();
        AutomaticMigrationsEnabled = false;
    }
}

3- 使用流畅的 API 创建注释:

public static void Configure(DbModelBuilder builder){    
builder.Entity<Company>().Property(c => c.Status).HasColumnAnnotation("Default", 0);            
}

请查看我的完整解决方案,我已经添加了所有实现细节,谢谢。 - Denny Puig

8
在 .NET Core 3.1 中,您可以在模型类中执行以下操作:
    public bool? Active { get; set; } 

在DbContext的OnModelCreating中添加默认值。
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Foundation>()
            .Property(b => b.Active)
            .HasDefaultValueSql("1");

        base.OnModelCreating(modelBuilder);
    }

导致数据库中出现以下结果

enter image description here

注意: 如果你的属性没有可空类型(bool?),你将会收到以下警告。
The 'bool' property 'Active' on entity type 'Foundation' is configured with a database-generated default. This default will always be used for inserts when the property has the value 'false', since this is the CLR default for the 'bool' type. Consider using the nullable 'bool?' type instead so that the default will only be used for inserts when the property value is 'null'.

3
问题是关于EF6而不是EF Core的。 - Jarrich Van de Voorde

6
很简单!只需使用所需的注释即可。
[Required]
public bool MyField { get; set; }

迁移的结果将是:

migrationBuilder.AddColumn<bool>(
name: "MyField",
table: "MyTable",
nullable: false,
defaultValue: false);

如果你想要真实的结果,请在更新数据库之前在迁移中将defaultValue更改为true


1
自动生成的迁移可以被更改,然后你将忘记默认值。 - Fernando Torres
简单易用,能够快速地为已有表格添加一个新列并在该新列上建立外键约束。谢谢。 - po10cySA
2
如果我们需要默认值为true怎么办? - Christos Lytras
1
@ChristosLytras 只需将该列重命名为 NotMyField /s - devklick

4
我发现在实体属性上仅使用自动属性初始化器就足以完成工作。
例如:
public class Thing {
    public bool IsBigThing{ get; set; } = false;
}

6
你觉得这应该能行,首先写好代码。但是在我的 EF 6.2 版本中并没有奏效。 - user1040323
这对于Code First EF 6实际上非常有效。我不确定为什么会有负分评价,我已经点了一个赞。 - Maverik
不支持 EF Core 5.0.13。最终我只能手动编辑迁移以使其具有...,defaultValue: false); - M Townsend

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接