如何将系统版本化时间 Table 与 Entity Framework 一起使用?
How can I use System-Versioned Temporal Table with Entity Framework?
我可以在 SQL Server 2016 中使用临时 tables。Entity Framework6 不幸的是还不知道这个功能。是否有可能使用新的查询选项(参见 msdn)和 Entity Framework 6?
我用临时员工创建了一个简单的演示项目 table:
我使用 edmx 将 table 映射到实体 ():
纯 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 的评论)
我尝试使用 Table 值函数:
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();
}
我必须为每个实体创建一个函数还是有通用选项?
不,恐怕你不能。在这方面,我一直与 Microsoft gurus
来回交流。
的解释使用 FromSql
是的,你可以稍加努力...
在尝试插入或更新生成的始终列时拦截 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)基于:
谢谢!
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
///
///
///
/// </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());
}
}
}
已在此处添加对时态表的初始支持:e7c0b9d (model/metadata part) and here 4b25a88(查询部分)并将在下一个预览版(预览版 8)以及当前的每晚版本中提供。
用法:
将实体映射到时间 table 可以在 OnModelCreating 中完成,如下所示:
modelBuilder.Entity<MyTemporalEntity>().ToTable(tb => tb.IsTemporal());
还支持其他配置 - 历史记录 table name/schema,期间开始和期间结束列的名称
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);
支持的操作:TemporalAsOf
、TemporalAll
、TemporalBetween
、TemporalFromTo
、TemporalContainedIn
。
一些限制和注意事项
使用临时操作的查询始终标记为“NoTracking”。可以从此类查询中 return 编辑具有相同密钥的多个实体,否则 EF 将无法正确解析它们的身份。
DbSet
直接支持时间操作,而不是 IQueryable
。在继承的情况下,它们不能应用于 OfType
操作。相反,使用:
context.Set<MyDerivedEntity>().TemporalAsOf(...);
导航扩展仅支持 AsOf
操作,因为它是唯一保证结果图一致性的时间操作。对于其他时间操作,必须使用 Join
.
手动创建导航
扩展导航时,目标实体也必须映射到时态table。时间操作从源传播到目标。不支持从时间实体导航到非时间实体。
context.Customers.TemporalAsOf(new DateTime(2020, 1, 1)).Select(c => c.Orders)
将于 2020 年 1 月 1 日 return 客户及其订单。临时操作会自动应用于客户和订单。
- 不支持映射到时间 table 的参数的集合操作(例如 Concat、Except)。 (此处跟踪问题 #25365)
引自maumar
我可以在 SQL Server 2016 中使用临时 tables。Entity Framework6 不幸的是还不知道这个功能。是否有可能使用新的查询选项(参见 msdn)和 Entity Framework 6?
我用临时员工创建了一个简单的演示项目 table:
我使用 edmx 将 table 映射到实体 (
纯 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 的评论)
我尝试使用 Table 值函数:
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();
}
我必须为每个实体创建一个函数还是有通用选项?
不,恐怕你不能。在这方面,我一直与 Microsoft gurus
来回交流。
FromSql
是的,你可以稍加努力...
在尝试插入或更新生成的始终列时拦截 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)基于:
谢谢!
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
///
///
///
/// </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());
}
}
}
已在此处添加对时态表的初始支持:e7c0b9d (model/metadata part) and here 4b25a88(查询部分)并将在下一个预览版(预览版 8)以及当前的每晚版本中提供。
用法:
将实体映射到时间 table 可以在 OnModelCreating 中完成,如下所示:
modelBuilder.Entity<MyTemporalEntity>().ToTable(tb => tb.IsTemporal());
还支持其他配置 - 历史记录 table name/schema,期间开始和期间结束列的名称
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);
支持的操作:TemporalAsOf
、TemporalAll
、TemporalBetween
、TemporalFromTo
、TemporalContainedIn
。
一些限制和注意事项
使用临时操作的查询始终标记为“NoTracking”。可以从此类查询中 return 编辑具有相同密钥的多个实体,否则 EF 将无法正确解析它们的身份。
DbSet
直接支持时间操作,而不是IQueryable
。在继承的情况下,它们不能应用于OfType
操作。相反,使用:
context.Set<MyDerivedEntity>().TemporalAsOf(...);
导航扩展仅支持
手动创建导航AsOf
操作,因为它是唯一保证结果图一致性的时间操作。对于其他时间操作,必须使用Join
.扩展导航时,目标实体也必须映射到时态table。时间操作从源传播到目标。不支持从时间实体导航到非时间实体。
context.Customers.TemporalAsOf(new DateTime(2020, 1, 1)).Select(c => c.Orders)
将于 2020 年 1 月 1 日 return 客户及其订单。临时操作会自动应用于客户和订单。
- 不支持映射到时间 table 的参数的集合操作(例如 Concat、Except)。 (此处跟踪问题 #25365)
引自maumar