如何让EF 6在插入操作时处理数据库中的DEFAULT CONSTRAINT

26

我对EF还不熟悉(这是我的第一周),但我对数据库和编程并不陌生。其他人也问过类似的问题,但我认为他们没有提供足够详细的信息或者解释得不够清楚,所以我来尝试一下。

问题: 当我执行INSERT操作时,如何让Entity Framework正确处理具有DEFAULT CONSTRAINT约束的数据库列?也就是说,如果我在模型中没有提供值,我如何让EF排除该列,从而使数据库定义的DEFAULT CONSTRAINT约束起作用?

背景

我创建了一个简单的表,只是为了测试Entity Framework 6(EF6)及其与SQL Server可更新列的交互。其中使用了IDENTITY、TIMESTAMP、COMPUTED以及一些应用了DEFAULT CONSTRAINT约束的列。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DBUpdateTest](
    [RowID] [int] IDENTITY(200,1) NOT NULL,
    [UserValue] [int] NOT NULL,
    [DefValue1] [int] NOT NULL,
    [DefValue2null] [int] NULL,
    [DefSecond] [int] NOT NULL,
    [CalcValue]  AS 
        (((([rowid]+[uservalue])+[defvalue1])+[defvalue2null])*[defsecond]),
    [RowTimestamp] [timestamp] NULL,
    CONSTRAINT [PK_DBUpdateTest] PRIMARY KEY CLUSTERED 
    (
        [RowID] ASC
    )
    WITH 
    (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,
    ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) 
) 
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD CONSTRAINT [DF_DBUpdateTest_DefValue1]      
DEFAULT ((200)) FOR [DefValue1]
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD CONSTRAINT [DF_DBUpdateTest_DefValue2null]  
DEFAULT ((30)) FOR [DefValue2null]
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD  CONSTRAINT [DF_DBUpdateTest_DefSecond]  
DEFAULT (datepart(second,getdate())) FOR [DefSecond]
GO

EF6完美处理IDENTITY、TIMESTAMP和COMPUTED列,这意味着在INSERT或UPDATE(通过context.SaveChanges())后,EF会将新的值读回实体对象中以供立即使用。

然而,对于具有DEFAULT CONSTRAINT的列,这种情况并不会发生。从我所了解到的情况来看,这是因为当EF生成TSQL执行INSERT时,它提供可空或不可空类型的通用默认值,就像该列没有DEFAULT CONSTRAINT定义一样。因此,EF完全忽略了DEFAULT CONSTRAINT的可能性。

以下是我用于插入DBUpdateTest记录的EF代码(我只更新了单个列):

DBUpdateTest myVal = new DBUpdateTest();
myVal.UserValue = RND.Next(20, 90);
DB.DBUpdateTests.Add(myVal);
DB.SaveChanges();

以下是在向DBUpdateTest进行插入时EF生成的SQL(它会更新所有可能的列):
 exec sp_executesql 
 N'INSERT [dbo].[DBUpdateTest]([UserValue], [DefValue1], [DefValue2null],
          [DefSecond])
   VALUES (@0, @1, NULL, @2)
   SELECT [RowID], [CalcValue], [RowTimestamp]
   FROM [dbo].[DBUpdateTest]
   WHERE @@ROWCOUNT > 0 AND [RowID] = scope_identity()',
 N'@0 int,@1 int,@2 int',@0=86,@1=0,@2=54

请注意,它非常清楚地提供了INT NOT NULL(0)和INT NULL(null)的默认值,这完全克服了DEFAULT CONSTRAINT。
当EF INSERT命令执行时,可空列将被提供NULL,而INT列将被提供ZERO。
RowID   UserValue   DefValue1   DefValue2null   DefSecond   CalcValue
=========================================================================
211     100         200         NULL            0           NULL

如果另一方面,我执行这个语句:
insert into DBUpdateTest (UserValue) values (100)

我会得到如下记录

RowID   UserValue   DefValue1   DefValue2null   DefSecond   CalcValue
=========================================================================
211     100         200         30              7           3787

这个方法之所以有效,是因为 TSQL INSERT 命令没有为任何具有定义的 DEFAULT CONSTRAINT 的列提供值。

因此,我想做的是,如果在模型对象中未显式设置这些列的值,则让 EF 排除 INSERT TSQL 中的 DEFAULT CONSTRAINT 列。

我已经尝试过的事情

1. 认识默认约束? SO:如何让 EF 处理默认约束

在我的 DbContext 类的 OnModelCreating() 方法中,建议我告诉 EF 具有 DEFAULT CONSTRAINT 的列是计算字段,但实际上它不是。但是,我想看看是否能让 EF 在 INSERT 后读取该值(不要紧,它也可能阻止我为该列分配值,这正是我不想要的):

        modelBuilder.Entity<DBUpdateTest>()
            .Property(e => e.DefValue1)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);

这段代码没有起作用,实际上看起来什么都没做(ED: 实际上它确实起了作用,请参考第2点)。EF仍然生成相同的TSQL,为列提供默认值并破坏数据库。
我是否遗漏了某个标志,忘记设置了某个配置项,可以使用某个函数属性,或者创建一些继承类代码,以使EF“正确处理DEFAULT CONSTRAINT列”? 2. 执行OnModelCreating()方法? SO: OnModelCreating未被调用 Janesh(下面)向我展示了EF将从其生成的TSQL INSERT命令中消除参数,如果该列标记有DatabaseGeneratedOption.Computed。但对我而言它不起作用,因为显然我使用的是错误类型的连接字符串(!)。
这是我的App.config,以及我展示“错误”和“正确”连接字符串的<connectionStrings>部分:
<connectionStrings>
  <add name="TEST_EF6Entities_NO_WORKY" providerName="System.Data.EntityClient" connectionString="metadata=res://*/TCXModel.csdl|res://*/TCXModel.ssdl|res://*/TCXModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=...ConnectStringHere...;App=EntityFramework&quot;"  />
  <add name="TEST_EF6Entities_IT_WORKS" providerName="System.Data.SqlClient" connectionString="data source=...ConnectStringHere...;App=EntityFramework;"  />
</connectionStrings>

区别:有效的使用了 System.Data.SqlClientProviderName,无效的使用了 System.Data.EntityClient。显然,SqlClient提供程序允许调用 OnModelCreating() 方法,这使得我可以使用 DatabaseGeneratedOption.Computed 生效。 ========== 未解决 ========== 列上默认约束的目的是允许我提供(或不提供)值,并仍在数据库端获得有效值。我不必知道SQL Server正在执行此操作,也不必知道默认值是什么或应该是什么。这完全发生在我的控制或知识之外。
关键是,我有选择的权利不提供值。我可以提供它,也可以不提供它,并且如果需要,我可以为每个INSERT提供不同的值。
使用DatabaseGeneratedOption.Computed对于这种情况真的不是一个有效的选择,因为它强制要求做出选择:“您始终可以提供一个值(因此永远不会利用数据库默认机制),或者永远不提供一个值(因此总是利用数据库默认机制)”。此外,该选项显然仅用于实际的计算列,并且不适用于具有DEFAULT CONSTRAINTs的列,因为一旦应用,则模型属性对于INSERT和UPDATE而言实际上变为只读-因为这就是真正的计算列的工作方式。显然,这妨碍了我选择向数据库提供或不提供值的权利。那么,我仍然想问:如何让EF“正确”地处理具有DEFAULT CONSTRAINT定义的数据库列?
3个回答

5
这段文字中的关键点是:
“因此,我想做的是让EF在生成INSERT TSQL语句时不包含默认约束列(如果我没有在对象中显式设置值)。 ”
Entity Framework不能为您完成。字段要么始终计算,要么始终包括在插入和更新中。但是,您可以编写类以按您所描述的方式运行。您必须在构造函数中或使用支持字段将字段(显式地)设置为默认值。
public class DBUpdateTest
/* public partial class DBUpdateTest*/ //version for database first
{
   private _DefValue1 = 200;
   private _DefValue2 = 30;

   public DbUpdateTest()
   {
      DefSecond = DateTime.Second;
   }

   public DefSecond { get; set; }

   public DefValue1
   {
      get { return _DefValue1; }
      set { _DefValue1 = value; }
   }

   public DefValue2
   {
      get { return _DefValue2; }
      set { _DefValue2 = value; }
   }
}

如果您始终使用这些类进行插入操作,则可能不需要在数据库中设置默认值;但如果您从其他地方使用sql进行插入操作,则还需要在数据库中添加默认约束。

2
这似乎是一个EF“Code First”的选项,如果我的应用程序负责数据库,我可以看到这将是一个很好的解决方案。但我正在使用“Database First”,因此我认为我的应用程序不应该知道关于列的数据库定义默认值的任何信息,更不用提供数据库已经定义的默认值。 - Mike Stillion
@MikeStillion 这不仅是一个 Code First 选项。你应该能够在部分类中做同样的事情。请查看这个答案https://dev59.com/bWnWa4cB1Zd3GeqPxBt1#12691948 - Colin
@MikeStillion 如果你不喜欢在两个地方设置值,目前唯一能做的就是投票支持改进:https://entityframework.codeplex.com/workitem/44 - Colin
@MikeStillion 和 EF7 正在逐步淘汰数据库优先模式。http://blogs.msdn.com/b/adonet/archive/2014/10/21/ef7-what-does-code-first-only-really-mean.aspx - Colin
好的,我知道我可以将默认值放在我的类中。但那不是我想做的。顺便说一句,你的谷歌功夫很强。我的最爱链接,最终显示这是一个问题,不会很快得到解决:https://entityframework.codeplex.com/workitem/44 - Mike Stillion
显示剩余2条评论

3
我强烈不同意DatabaseGeneratedOption.Computed不能帮助停止将字段发送到插入SQL命令中的说法。 我尝试了一个最小的示例来验证它,它起作用了。
注意:一旦您将DatabaseGeneratedOption.Computed应用于任何属性,则无法从EF指定任何值。即在插入或更新记录时无法指定任何值。
模型
public class Person
{
    public int Id { get; set; }
    public int SomeId { get; set; }
    public string Name { get; set; }
}

背景

public class Context : DbContext
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>().HasKey(d => d.Id);
        modelBuilder.Entity<Person>()
            .Property(d => d.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        modelBuilder.Entity<Person>()
            .Property(d => d.SomeId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
    }
}

迁移

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.People",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    SomeId = c.Int(nullable: false, defaultValue:3), //I edited it mannually to assign default value 3.
                    Name = c.String(),
                })
            .PrimaryKey(t => t.Id);

    }

    public override void Down()
    {
        DropTable("dbo.People");
    }
}

注意:我手动将默认值3编辑为SomeId。

MainProgram

    static void Main(string[] args)
    {
        using (Context c = new Context())
        {
            Person p = new Person();
            p.Name = "Jenish";
            c.People.Add(p);
            c.Database.Log = Console.WriteLine;
            c.SaveChanges();
        }
    }

我在控制台中记录了以下查询:

Opened connection at 04/15/2015 11:32:19 AM +05:30

Started transaction at 04/15/2015 11:32:19 AM +05:30

INSERT [dbo].[People]([Name])
VALUES (@0)
SELECT [Id], [SomeId]
FROM [dbo].[People]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: 'Jenish' (Type = String, Size = -1)

-- Executing at 04/15/2015 11:32:20 AM +05:30

-- Completed in 3 ms with result: SqlDataReader



Committed transaction at 04/15/2015 11:32:20 AM +05:30

Closed connection at 04/15/2015 11:32:20 AM +05:30

注意:在Insert命令中未传递SomeId,而是在select命令中选择。


谢谢Jenish向我展示Database.Log = Console.WriteLine选项。这比使用SQL Profiler捕获TSQL输出要容易得多。 - Mike Stillion
1
在想为什么你的代码能够工作而我的不能之后,我在我的OnModelCreating()方法中设置了一个断点...然后发现它根本没有被调用!原来OnModelCreating()只有在使用类型为System.Data.SqlClient的连接字符串时才会被调用,但我的连接字符串是System.Data.EntityClient类型。所以一旦我解决了这个问题,我的代码就真正起作用了,正如Jenish所说,使用DatabaseGeneratedOption.Computed选项确实可以防止它被包含在EF生成的TSQL INSERT中。 - Mike Stillion

-1

如果有人遇到这个问题...

您可以将列属性设置为StoreGeneratedPattern="Computed",但这是一个二选一的情况。 在某些情况下,您可能需要覆盖列的默认值/约束(我们大多数人都会指定这样的约束,以便99.9%的时间不必发送值)。然而上述修复方法并不允许这样做 :(

因此,一旦设置为'Computed',该列将明确忽略所有插入/更新操作...


2
但这总是设置默认值。设置其他值是不可能的,这不是问题的意图。 - Gert Arnold

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