实体框架不使用时态表

Posted

技术标签:

【中文标题】实体框架不使用时态表【英文标题】:Entity Framework not working with temporal table 【发布时间】:2017-04-06 03:28:52 【问题描述】:

我正在使用数据库优先实体框架 6。将架构中的一些表更改为临时表后,我在尝试插入新数据时开始收到以下错误:

Cannot insert an explicit value into a GENERATED ALWAYS column in table '<MyDatabase>.dbo.<MyTableName>. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column.

看起来 EF 正在尝试更新由系统管理的 PERIOD 列的值。

从 EDMX 文件中删除列似乎可以解决问题,但这不是一个可行的解决方案,因为每次从数据库重新生成模型时都会重新添加列。

【问题讨论】:

【参考方案1】:

这个问题有两种解决方案:

    在 EDMX 设计器中列的属性窗口中,将 PERIOD 列(在我的示例中为 ValidFrom 和 ValidTo)上的 StoreGeneratedPattern 更改为 identity。标识优于计算,因为计算将导致 EF 刷新插入和更新上的值,而不是仅使用 identity 的插入 创建一个IDbCommandTreeInterceptor 实现以删除句点列。这是我的首选解决方案,因为在向模型添加新表时不需要额外的工作。

这是我的实现:

using System.Data.Entity.Infrastructure.Interception; 
using System.Data.Entity.Core.Common.CommandTrees; 
using System.Data.Entity.Core.Metadata.Edm; 
using System.Collections.ObjectModel;

internal class TemporalTableCommandTreeInterceptor : IDbCommandTreeInterceptor

    private static readonly List<string> _namesToIgnore = new List<string>  "ValidFrom", "ValidTo" ;

    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    
        if (interceptionContext.OriginalResult.DataSpace == DataSpace.SSpace)
        
            var insertCommand = interceptionContext.Result as DbInsertCommandTree;
            if (insertCommand != null)
            
                var newSetClauses = GenerateSetClauses(insertCommand.SetClauses);

                var newCommand = new DbInsertCommandTree(
                    insertCommand.MetadataWorkspace,
                    insertCommand.DataSpace,
                    insertCommand.Target,
                    newSetClauses,
                    insertCommand.Returning);

                interceptionContext.Result = newCommand;
            

            var updateCommand = interceptionContext.Result as DbUpdateCommandTree;
            if (updateCommand != null)
            
                var newSetClauses = GenerateSetClauses(updateCommand.SetClauses);

                var newCommand = new DbUpdateCommandTree(
                    updateCommand.MetadataWorkspace,
                    updateCommand.DataSpace,
                    updateCommand.Target,
                    updateCommand.Predicate,
                    newSetClauses,
                    updateCommand.Returning);

                interceptionContext.Result = newCommand;
            
        
    

    private static ReadOnlyCollection<DbModificationClause> GenerateSetClauses(IList<DbModificationClause> modificationClauses)
    
        var props = new List<DbModificationClause>(modificationClauses);
        props = props.Where(_ => !_namesToIgnore.Contains((((_ as DbSetClause)?.Property as DbPropertyExpression)?.Property as EdmProperty)?.Name)).ToList();

        var newSetClauses = new ReadOnlyCollection<DbModificationClause>(props);
        return newSetClauses;
    

在使用上下文之前,通过在代码中的任何位置运行以下命令,向 EF 注册此拦截器:

DbInterception.Add(new TemporalTableCommandTreeInterceptor());

【讨论】:

我怎样才能对实体框架核心做同样的事情? @AramGevorgyan - 您可以在属性上使用属性 [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 或使用 Fluent API 方法 .ValueGeneratedOnAddOrUpdate() 例如entity.Property(e => e.ValidFrom).ValueGeneratedOnAddOrUpdate(); see here供参考。 工作就像一个魅力! usings 如下using System.Data.Entity.Infrastructure.Interception; using System.Data.Entity.Core.Common.CommandTrees; using System.Data.Entity.Core.Metadata.Edm; using System.Collections.ObjectModel;【参考方案2】:

我在系统版本表上遇到了这个错误,我只是将 EF 配置设置为忽略系统维护的列,就像这样

            Ignore(x => x.SysEndTime);
            Ignore(x => x.SysStartTime);

并且插入/更新与数据库一起工作,仍然根据需要更新这些列以保留历史记录。 另一种方法是像这样设置列

Property(x => x.SysEndTime).IsRequired().HasColumnType("datetime2").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);

【讨论】:

对我来说完美的解决方案!完美运行。 数据模型中的[DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime? SysStartTime get; set; 会不会有同样的效果?见docs.microsoft.com/en-us/ef/core/modeling/…【参考方案3】:

另一种解决方案是在表的字段中创建默认约束。

CREATE TABLE [dbo].[Table] (
    [Id]            INT IDENTITY(1, 1)  NOT NULL,
    [Description]   NVARCHAR(100)       NOT NULL,
    [ValidFrom]     DATETIME2(0)        GENERATED ALWAYS AS ROW START HIDDEN CONSTRAINT [Df_Table_ValidFrom] DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    [ValidTo]       DATETIME2(0)        GENERATED ALWAYS AS ROW END HIDDEN CONSTRAINT [Df_Table_ValidTo] DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),
    CONSTRAINT [Pk_Table] PRIMARY KEY CLUSTERED ([Id] ASC)
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[Table_History]));
GO

在代码中不需要改动什么。

【讨论】:

创建了这个表,但是插入了 db.Tables.Add(new Table() Description = "des", Id = 1);仍然给出错误无法将显式值插入... 即使使用默认约束? 好吧,我刚刚发布了我的答案,它可以用于插入,但不能更新。您的回答是我解决方案的一部分,谢谢!【参考方案4】:

我确实设法在没有任何开销的情况下将时态表与实体框架一起使用。

    正如 José Ricardo Garcia 所说,使用默认约束

    另一种解决方案是在表的字段中创建默认约束。

    这是更改表格而不是创建表格的脚本。

    ALTER TABLE [dbo].[Table]
    ADD ValidFrom DATETIME2(0) GENERATED ALWAYS AS ROW START HIDDEN CONSTRAINT [Df_Table_ValidFrom] DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    ValidTo   DATETIME2(0) GENERATED ALWAYS AS ROW END HIDDEN CONSTRAINT [Df_Table_ValidTo] DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    go
    ALTER TABLE [dbo].[Table]
    SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE=dbo.[TableHistory]))
    GO
    

    正如 Matt Ruwe 所说,在 edmx 中将列切换为身份

    在 EDMX 设计器中列的属性窗口中,将 PERIOD 列(在我的例子中为 ValidFrom 和 ValidTo)上的 StoreGeneratedPattern 更改为标识。标识比计算好,因为计算将导致 EF 刷新插入和更新上的值,而不是仅使用标识的插入

    由于上述两种方法在插入时工作得很好,它们对更新实体不起作用。我不得不手动告诉这两列没有被修改,

    Entry(existingResult).CurrentValues.SetValues(table);
    Entry(existingResult).Property(x => x.ValidTo).IsModified = false;
    Entry(existingResult).Property(x => x.ValidFrom).IsModified = false;
    

现在我可以成功调用db.SaveChanges() 并摆脱错误,即使实体已被修改。希望对您有所帮助! 注意:我使用 DbFirst 和 EF6

【讨论】:

【参考方案5】:

制作期间开始列 (ValidFrom) 和期间结束列 (ValidTo) 应该可以解决此问题。我们可以这样做

ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidFrom] ADD HIDDEN;
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidTo] ADD HIDDEN;

我们可以在 sys.columns 表中看到隐藏这些列的设置

SELECT * FROM sys.columns WHERE is_hidden = 1

【讨论】:

以上是关于实体框架不使用时态表的主要内容,如果未能解决你的问题,请参考以下文章

Net Core:实体框架和 SQL Server 时态表、自动脚手架

从实体框架代码第一方法的时态表中获取SYSTEM_TIME信息

如何使用 JPA 实现时态表?

实体框架代码首先不使用 VS 2012 创建表

删除系统版本化时态表的过程

使用 CTE 优化时态表