如何将系统版本化的时态表与实体框架一起使用?

Posted

技术标签:

【中文标题】如何将系统版本化的时态表与实体框架一起使用?【英文标题】:How can I use System-Versioned Temporal Table with Entity Framework? 【发布时间】:2017-05-21 17:50:16 【问题描述】:

我可以在 SQL Server 2016 中使用临时表。不幸的是,Entity Framework 6 还不知道这个功能。是否有可能在 Entity Framework 6 中使用新的查询选项(请参阅msdn)?

我用员工临时表创建了一个简单的演示项目:

我使用 edmx 将表格映射到实体 (thanks to Matt Ruwe):

使用纯 sql 语句一切正常:

using (var context = new TemporalEntities())

    var employee = context.Employees.Single(e => e.EmployeeID == 2);
    var query = 
      $@"SELECT * FROM [TemporalTest].[dbo].[nameof(Employee)]
         FOR SYSTEM_TIME BETWEEN
         '0001-01-01 00:00:00.00' AND 'employee.ValidTo:O'
         WHERE EmployeeID = 2";
    var historyOfEmployee = context.Employees.SqlQuery(query).ToList();
    

是否可以在没有纯 SQL 的情况下为每个实体添加历史记录功能?我的解决方案作为带有反射的实体扩展来操纵来自IQuerable 的SQL 查询并不完美。 是否有现有的扩展程序或库来执行此操作?

编辑:(根据Pawel的评论)

我尝试使用表值函数:

CREATE FUNCTION dbo.GetEmployeeHistory(
    @EmployeeID int, 
    @startTime datetime2, 
    @endTime datetime2)
RETURNS TABLE
AS
RETURN 
(
    SELECT 
        EmployeeID,
        [Name], 
        Position, 
        Department, 
        [Address],
        ValidFrom,
        ValidTo
    FROM dbo.Employee
    FOR SYSTEM_TIME BETWEEN @startTime AND @endTime
    WHERE EmployeeID = @EmployeeID
);
using (var context = new TemporalEntities())

    var employee = context.Employees.Single(e => e.EmployeeID == 2);
    var historyOfEmployee =
      context.GetEmployeeHistory(2, DateTime.MinValue, employee.ValidTo).ToList();
 

我必须为每个实体创建一个函数还是有一个通用选项?

【问题讨论】:

您考虑过使用 TVF 吗? 可以得到正确的实体类型吗?在此示例中,GetEmployeeHistory 会重新运行 Employee 而不是“未知”类型? 是的。可以获得正确的实体类型。我不记得了,但设计师可能想为您的 TVF 模型添加一个类型。您不希望这样 - 您想使用现有类型。重要的是 TVF 会返回正确的列,以便实现实体。 小更新,如果你使用 EFCore:nuget.org/packages/EFCoreTemporalSupport 【参考方案1】:

不,恐怕你不能。我在这方面与Microsoft gurus 来来回回。

这是一个已知的issue。 我发现的最佳建议是使用FromSql,如here 所述。

【讨论】:

【参考方案2】:

是的,您可以稍加努力......

在尝试插入或更新始终生成的列时拦截 EFF 意图并避免类似的错误

"Cannot insert an explicit value into a GENERATED ALWAYS column in table 'xxx.dbo.xxxx'. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column."

之后它就像魅力一样工作(已在 Azure Db 上投入生产)

实施示例 EFF6 基于列(StartTime y EndTime)基于:

entity-framework-not-working-with-temporal-table

insert-record-in-temporal-table-using-c-sharp-entity-framework

dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified

谢谢!

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

namespace Ubiquité.Clases


    /// <summary>
    /// Evita que EFF se haga cargo de ciertos campos que no debe tocar Ej: StartTime y EndTime
    ///     de las tablas versionadas o bien los row_version por ejemplo
    ///     https://***.com/questions/40742142/entity-framework-not-working-with-temporal-table
    ///     https://***.com/questions/44253965/insert-record-in-temporal-table-using-c-sharp-entity-framework
    ///     https://***.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified
    /// </summary>
    /// <remarks>
    /// "Cannot insert an explicit value into a GENERATED ALWAYS column in table 'xxx.dbo.xxxx'.
    /// Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT
    /// into GENERATED ALWAYS column."
    /// </remarks>
    internal class TemporalTableCommandTreeInterceptor : IDbCommandTreeInterceptor
    
        private static readonly List<string> _namesToIgnore = new List<string>  "StartTime", "EndTime" ;

        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;
        
    

    /// <summary>
    /// registra TemporalTableCommandTreeInterceptor con EFF
    /// </summary>
    public class MyDBConfiguration : DbConfiguration
    
        public MyDBConfiguration()
        
            DbInterception.Add(new TemporalTableCommandTreeInterceptor());
        
    


【讨论】:

【参考方案3】:

已在此处添加了对临时表的初始支持:e7c0b9d(模型/元数据部分)和此处4b25a88(查询部分),并将在下一个预览版(预览版 8)以及当前的每晚位。

用法:

可以在 OnModelCreating 中将实体映射到时态表,如下所示:

modelBuilder.Entity<MyTemporalEntity>().ToTable(tb => tb.IsTemporal());

还支持其他配置 - 历史表名称/模式、期间开始和期间结束列的名称

modelBuilder.Entity<MyTemporalEntity>().ToTable(tb => tb.IsTemporal(ttb =>

    ttb.HasPeriodStart("SystemTimeStart");
    ttb.HasPeriodEnd("SystemTimeEnd");
    ttb.WithHistoryTable("MyHistoryTable", "mySchema");
));

支持迁移,因此可以将现有实体转换为临时实体。

查询:

var myDate = new DateTime(2020, 1, 1);
context.MyTemporalEntities.TemporalAsOf(myDate).Where(e => e.Id < 10);

支持的操作:TemporalAsOfTemporalAllTemporalBetweenTemporalFromToTemporalContainedIn

一些限制和注意事项

使用时间操作的查询总是被标记为“NoTracking”。此类查询可能会返回具有相同密钥的多个实体,否则 EF 将无法正确解析其身份。

DbSet 直接支持临时操作,而不是 IQueryable。在继承的情况下,它们不能应用于OfType 操作。相反,使用:

context.Set<MyDerivedEntity>().TemporalAsOf(...);

导航扩展仅支持AsOf 操作,因为它是唯一保证结果图一致性的时间操作。对于其他时间操作,导航必须使用Join 手动创建。

扩展导航时,目标实体也必须映射到时态表。时间操作从源传播到目标。不支持从时间实体导航到非时间实体。

context.Customers.TemporalAsOf(new DateTime(2020, 1, 1)).Select(c => c.Orders)

将于 2020 年 1 月 1 日返回客户及其订单。临时操作会自动应用于客户和订单。

不支持对映射到时态表的参数进行设置操作(例如 Concat、Except)。 (此处跟踪问题#25365)

引用maumar

【讨论】:

以上是关于如何将系统版本化的时态表与实体框架一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

实体框架不使用时态表

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

如何将 SQL Server Compact 3.5 与实体框架一起使用

在 EF6 中使用时态表 - PostgreSQL

如何将系统表或信息架构表与 Redshift 中的用户定义表连接起来

在 Entity Framework Core 中查询系统版本时态表中的数据