[英]Querying Data in a System-Versioned Temporal Table in Entity Framework Core
[英]How can I use System-Versioned Temporal Table with Entity Framework?
我可以在 SQL Server 2016 中使用時態表。不幸的是,Entity Framework 6 還不知道這個功能。 在 Entity Framework 6 中使用新的查詢選項(請參閱msdn )是否有解決方法的可能性?
我使用員工臨時表創建了一個簡單的演示項目:
我使用 edmx 將表映射到實體( 感謝 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();
}
我必須為每個實體創建一個函數還是有一個通用選項?
是的,你可以稍微努力一下......
在嘗試插入或更新生成的始終列時攔截 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 上生產)
基於列 (StartTime y EndTime)的實現EFF6示例基於:
使用 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://stackoverflow.com/questions/40742142/entity-framework-not-working-with-temporal-table
/// https://stackoverflow.com/questions/44253965/insert-record-in-temporal-table-using-c-sharp-entity-framework
/// https://stackoverflow.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());
}
}
}
我只使用 Entity Framework Core 對此進行了測試,但我認為應該可以使用類似的解決方案。
正如@StephenWitherden 所提到的,目前有一個未解決的問題來支持 EF Core 的開箱即用:
https://github.com/dotnet/efcore/issues/4693
我寫了一個完整的指南,如何在沒有任何第三方庫的情況下使用 Entity Framework Core 實現它:
在此處添加了對臨時表的初始支持: 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);
支持的操作: TemporalAsOf
、 TemporalAll
、 TemporalBetween
、 TemporalFromTo
、 TemporalContainedIn
。
一些限制和注意事項
使用時間操作的查詢總是被標記為“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 日返回客戶及其訂單。臨時操作會自動應用於客戶和訂單。
來自毛馬爾的報價
使用DatabaseGenerated
屬性為您的實體類型添加元數據類
[MetadataType(typeof(Delegations_Metadata))]
public partial class Delegations
{
public partial class Delegations_Metadata
{
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime SysStartTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime SysEndTime { get; set; }
}
}
覆蓋DbContext.SaveChanges
以從創建和更新命令中顯式排除具有DatabaseGenerated
屬性的屬性
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries()
.Where(x => x.State == EntityState.Modified || x.State == EntityState.Added))
{
var notMappedProperties = entry.Entity.GetType().GetProperties()
.Where(x => x.GetCustomAttribute<DatabaseGeneratedAttribute>()
?.DatabaseGeneratedOption == DatabaseGeneratedOption.Computed);
foreach (var notMappedProperty in notMappedProperties)
entry.Property(notMappedProperty.Name).IsModified = false;
}
return base.SaveChanges();
}
此解決方案適用於:
GENERATED ALWAYS
列的系統版本表
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.