Entity Framework 不使用时间 table
Entity Framework not working with temporal table
我首先使用数据库 entity framework 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 文件中删除列似乎可以解决问题,但这不是一个可行的解决方案,因为每次从数据库重新生成模型时都会重新添加这些列。
这个问题有两种解决方法:
- 在 EDMX 设计器列的 属性 window 中,将
PERIOD
列(在我的例子中是 ValidFrom 和 ValidTo)上的 StoreGeneratedPattern
更改为identity
。 Identity 优于 computed,因为 computed 会导致 EF 刷新 Insert 和 Update 上的值,而不是仅使用 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());
另一个解决方案是在 table 的字段中创建默认约束。
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
代码中不需要改动什么。
创建期间开始列 (ValidFrom) 和期间结束列 (ValidTo) 应该可以解决此问题。我们可以通过
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidFrom] ADD HIDDEN;
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidTo] ADD HIDDEN;
我们可以在 sys.columns table
中看到这些列的隐藏设置
SELECT * FROM sys.columns WHERE is_hidden = 1
我确实设法在没有任何开销的情况下将时间 table 与实体框架一起使用。
使用默认约束,正如 José Ricardo Garcia 所说
An other solution is create default constraint in the fields of the table.
这是修改 table 而不是创建 table 的脚本。
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
在 edmx 中将列切换为标识,如 Matt Ruwe 所说
In the property window for the column in the EDMX designer, change the StoreGeneratedPattern on the PERIOD columns (ValidFrom and ValidTo in my case) to be identity. Identity is better than computed since computed will cause EF to refresh the values on an Insert and Update as opposed to just an insert with identity
由于上述两种方法都可以很好地用于插入,它们不适用于更新实体。我不得不手动告诉这两列没有修改,
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
我 运行 在系统版本 table 上遇到此错误,我只是将 EF 配置设置为忽略系统维护的列,就像这样
Ignore(x => x.SysEndTime);
Ignore(x => x.SysStartTime);
和 insert/update 与 DB 一起工作,仍然根据需要更新这些列以保留历史记录。
另一种方法是像这样设置列
Property(x => x.SysEndTime).IsRequired().HasColumnType("datetime2").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
我首先使用数据库 entity framework 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 文件中删除列似乎可以解决问题,但这不是一个可行的解决方案,因为每次从数据库重新生成模型时都会重新添加这些列。
这个问题有两种解决方法:
- 在 EDMX 设计器列的 属性 window 中,将
PERIOD
列(在我的例子中是 ValidFrom 和 ValidTo)上的StoreGeneratedPattern
更改为identity
。 Identity 优于 computed,因为 computed 会导致 EF 刷新 Insert 和 Update 上的值,而不是仅使用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());
另一个解决方案是在 table 的字段中创建默认约束。
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
代码中不需要改动什么。
创建期间开始列 (ValidFrom) 和期间结束列 (ValidTo) 应该可以解决此问题。我们可以通过
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidFrom] ADD HIDDEN;
ALTER TABLE [dbo].[Table1] ALTER COLUMN [ValidTo] ADD HIDDEN;
我们可以在 sys.columns table
中看到这些列的隐藏设置SELECT * FROM sys.columns WHERE is_hidden = 1
我确实设法在没有任何开销的情况下将时间 table 与实体框架一起使用。
使用默认约束,正如 José Ricardo Garcia 所说
An other solution is create default constraint in the fields of the table.
这是修改 table 而不是创建 table 的脚本。
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
在 edmx 中将列切换为标识,如 Matt Ruwe 所说
In the property window for the column in the EDMX designer, change the StoreGeneratedPattern on the PERIOD columns (ValidFrom and ValidTo in my case) to be identity. Identity is better than computed since computed will cause EF to refresh the values on an Insert and Update as opposed to just an insert with identity
由于上述两种方法都可以很好地用于插入,它们不适用于更新实体。我不得不手动告诉这两列没有修改,
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
我 运行 在系统版本 table 上遇到此错误,我只是将 EF 配置设置为忽略系统维护的列,就像这样
Ignore(x => x.SysEndTime);
Ignore(x => x.SysStartTime);
和 insert/update 与 DB 一起工作,仍然根据需要更新这些列以保留历史记录。 另一种方法是像这样设置列
Property(x => x.SysEndTime).IsRequired().HasColumnType("datetime2").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);