为什么这个带有 IDENTITY_INSERT 的 EF 插入不起作用?
Why does this EF insert with IDENTITY_INSERT not work?
这是查询:
using (var db = new AppDbContext())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
}
执行时,插入记录的Id
,在新的table上,仍然是1。
NEW: 当我使用事务或 TGlatzer 的答案时,我得到异常:
Explicit value must be specified for identity column in table 'Items'
either when IDENTITY_INSERT is set to ON or when a replication user is
inserting into a NOT FOR REPLICATION identity column.
根据之前的 Question,您需要开始您的上下文事务。保存更改后,您还必须重新声明 Identity Insert 列,最后您必须提交事务。
using (var db = new AppDbContext())
using (var transaction = db .Database.BeginTransaction())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
transaction.Commit();
}
我不尊重这个关于 EF6 的问题的标签。
此答案适用于 EF Core
这里的真正罪魁祸首不是丢失的事务,而是小的不便,即 Database.ExectueSqlCommand()
不会保持连接打开,之前没有明确打开。
using (var db = new AppDbContext())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.OpenConnection();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
}
也可以,因为 SET IDENTITY_INSERT [...] ON/OFF
将绑定到您的连接。
即使你关掉IDENTITY_INSERT
,你也只是告诉SQL我会把Identity发给你,你并没有告诉entity framework把Identity发给SQL服务器。
所以基本上,您必须如下所示创建 DbContext ..
// your existing context
public abstract class BaseAppDbContext : DbContext {
private readonly bool turnOfIdentity = false;
protected AppDbContext(bool turnOfIdentity = false){
this.turnOfIdentity = turnOfIdentity;
}
public DbSet<IdentityItem> IdentityItems {get;set;}
protected override void OnModelCreating(DbModelBuilder modelBuilder){
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityItem>()
.HasKey( i=> i.Id )
// BK added the "Property" line.
.Property(e => e.Id)
.HasDatabaseGeneratedOption(
turnOfIdentity ?
DatabaseGeneratedOption.None,
DatabaseGeneratedOption.Identity
);
}
}
public class IdentityItem{
}
public class AppDbContext: BaseAppDbContext{
public AppDbContext(): base(false){}
}
public class AppDbContextWithIdentity : BaseAppDbContext{
public AppDbContext(): base(true){}
}
现在这样使用...
using (var db = new AppDbContextWithIdentity())
{
using(var tx = db.Database.BeginTransaction()){
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
tx.Commit();
}
}
要强制 EF 写入您实体的 ID,您必须将 ID 配置为不存储生成,否则 EF 永远不会在插入语句中包含该 ID。
因此,您需要即时更改模型并根据需要配置实体 ID。
问题是模型被缓存了,动态更改它非常棘手(我很确定我已经做到了,但实际上我找不到代码,可能是我把它扔掉了)。最短的方法是创建两个不同的上下文,您可以在其中以两种不同的方式配置实体,如 DatabaseGeneratedOption.None
(当您需要编写 ID 时)和 DatabaseGeneratedOption.Identity
(当您需要自动编号 ID 时)。
绝不能在生产代码中使用,这只是为了好玩
我看到我的仍然是公认的答案,再次,不要使用这个(解决这个问题),查看下面的其他答案
我不建议这样做,因为这是一个疯狂的黑客,但无论如何。
我想我们可以通过截取SQL命令并更改命令文本来实现
(您可以继承 DbCommandInterceptor 并覆盖 ReaderExecuting)
目前我没有可用的示例,我必须去,但我认为它是可行的
示例代码
public class MyDbInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
if (is your table)
{
command.CommandText = "Set Identity off ,update insert into ,Set Identity off"
return;
}
base.ReaderExecuting(command, interceptionContext);
}
}
ORM 是一个很好的抽象,我真的很喜欢它们,但我认为尝试 "hack" 它们来支持较低(更接近数据库)级别的操作没有意义。
我尽量避免存储过程,但我认为在这种情况下(正如你所说的例外)我认为你应该使用 one
我有一个非常相似的问题。
解决方案是这样的:
db.Database.ExecuteSqlCommand("disable trigger all on myTable ;")
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable OFF");
db.Database.ExecuteSqlCommand("enable trigger all on myTable ;")
在我的例子中,消息 Explicit value must be specified for identity...
是因为在插入时调用了一个触发器并会插入其他内容。
ALTER TABLE myTable NOCHECK CONSTRAINT all
也很有用
答案适用于 Entity Framework 6
只需使用 IDENTITY_INSERT 外部交易
using (var db = new AppDbContext())
{
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
using (var transaction = db .Database.BeginTransaction())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.SaveChanges();
transaction.Commit();
}
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
}
我遇到了类似的问题。在我的生产代码中,实体依赖于身份生成。但是对于集成测试,我需要手动设置一些 ID。在我不需要明确设置它们的地方,我在 test data builders 中生成了它们。为了实现这一点,我创建了一个 DbContext
继承我的生产代码中的那个,并为每个实体配置身份生成,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Entity1>().Property(e => e.Id).ValueGeneratedNever();
modelBuilder.Entity<Entity2>().Property(e => e.Id).ValueGeneratedNever();
...
}
但这还不够,我不得不禁用 SQL 服务器 IDENTITY_INSERT
。这在单个 table 中插入数据时有效。但是当你有彼此相关的实体并且你想插入一个对象图时,这在 DbContext.SaveChanges()
上失败了。原因是根据 SQL Server documentation you can have IDENTITY_INSERT ON
just for one table at a time during a session. My colleague suggested to use a DbCommandInterceptor
which is similar to the . I made it work for INSERT INTO
only but the concept could be expanded further. Currently it intercepts and modifies multiple INSERT INTO
statements within a single DbCommand.CommandText
. The code could be optimized to use Span.Slice 为了避免由于字符串操作而占用太多内存,但由于我找不到 Split
方法,所以我没有花时间在这上面。无论如何,我正在使用这个 DbCommandInterceptor
进行集成测试。觉得对您有帮助,欢迎采纳。
/// <summary>
/// When enabled intercepts each INSERT INTO statement and detects which table is being inserted into, if any.
/// Then adds the "SET IDENTITY_INSERT table ON;" (and same for OFF) statement before (and after) the actual insertion.
/// </summary>
public class IdentityInsertInterceptor : DbCommandInterceptor
{
public bool IsEnabled { get; set; }
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
if (IsEnabled)
{
ModifyAllStatements(command);
}
return base.ReaderExecuting(command, eventData, result);
}
private static void ModifyAllStatements(DbCommand command)
{
string[] statements = command.CommandText.Split(';', StringSplitOptions.RemoveEmptyEntries);
var commandTextBuilder = new StringBuilder(capacity: command.CommandText.Length * 2);
foreach (string statement in statements)
{
string modified = ModifyStatement(statement);
commandTextBuilder.Append(modified);
}
command.CommandText = commandTextBuilder.ToString();
}
private static string ModifyStatement(string statement)
{
const string insertIntoText = "INSERT INTO [";
int insertIntoIndex = statement.IndexOf(insertIntoText, StringComparison.InvariantCultureIgnoreCase);
if (insertIntoIndex < 0)
return $"{statement};";
int closingBracketIndex = statement.IndexOf("]", startIndex: insertIntoIndex, StringComparison.InvariantCultureIgnoreCase);
string tableName = statement.Substring(
startIndex: insertIntoIndex + insertIntoText.Length,
length: closingBracketIndex - insertIntoIndex - insertIntoText.Length);
// we should probably check whether the table is expected - list with allowed/disallowed tables
string modified = $"SET IDENTITY_INSERT [{tableName}] ON; {statement}; SET IDENTITY_INSERT [{tableName}] OFF;";
return modified;
}
}
这是查询:
using (var db = new AppDbContext())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
}
执行时,插入记录的Id
,在新的table上,仍然是1。
NEW: 当我使用事务或 TGlatzer 的答案时,我得到异常:
Explicit value must be specified for identity column in table 'Items' either when IDENTITY_INSERT is set to ON or when a replication user is inserting into a NOT FOR REPLICATION identity column.
根据之前的 Question,您需要开始您的上下文事务。保存更改后,您还必须重新声明 Identity Insert 列,最后您必须提交事务。
using (var db = new AppDbContext())
using (var transaction = db .Database.BeginTransaction())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
transaction.Commit();
}
我不尊重这个关于 EF6 的问题的标签。
此答案适用于 EF Core
这里的真正罪魁祸首不是丢失的事务,而是小的不便,即 Database.ExectueSqlCommand()
不会保持连接打开,之前没有明确打开。
using (var db = new AppDbContext())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.OpenConnection();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
}
也可以,因为 SET IDENTITY_INSERT [...] ON/OFF
将绑定到您的连接。
即使你关掉IDENTITY_INSERT
,你也只是告诉SQL我会把Identity发给你,你并没有告诉entity framework把Identity发给SQL服务器。
所以基本上,您必须如下所示创建 DbContext ..
// your existing context
public abstract class BaseAppDbContext : DbContext {
private readonly bool turnOfIdentity = false;
protected AppDbContext(bool turnOfIdentity = false){
this.turnOfIdentity = turnOfIdentity;
}
public DbSet<IdentityItem> IdentityItems {get;set;}
protected override void OnModelCreating(DbModelBuilder modelBuilder){
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityItem>()
.HasKey( i=> i.Id )
// BK added the "Property" line.
.Property(e => e.Id)
.HasDatabaseGeneratedOption(
turnOfIdentity ?
DatabaseGeneratedOption.None,
DatabaseGeneratedOption.Identity
);
}
}
public class IdentityItem{
}
public class AppDbContext: BaseAppDbContext{
public AppDbContext(): base(false){}
}
public class AppDbContextWithIdentity : BaseAppDbContext{
public AppDbContext(): base(true){}
}
现在这样使用...
using (var db = new AppDbContextWithIdentity())
{
using(var tx = db.Database.BeginTransaction()){
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
tx.Commit();
}
}
要强制 EF 写入您实体的 ID,您必须将 ID 配置为不存储生成,否则 EF 永远不会在插入语句中包含该 ID。
因此,您需要即时更改模型并根据需要配置实体 ID。
问题是模型被缓存了,动态更改它非常棘手(我很确定我已经做到了,但实际上我找不到代码,可能是我把它扔掉了)。最短的方法是创建两个不同的上下文,您可以在其中以两种不同的方式配置实体,如 DatabaseGeneratedOption.None
(当您需要编写 ID 时)和 DatabaseGeneratedOption.Identity
(当您需要自动编号 ID 时)。
绝不能在生产代码中使用,这只是为了好玩
我看到我的仍然是公认的答案,再次,不要使用这个(解决这个问题),查看下面的其他答案
我不建议这样做,因为这是一个疯狂的黑客,但无论如何。
我想我们可以通过截取SQL命令并更改命令文本来实现
(您可以继承 DbCommandInterceptor 并覆盖 ReaderExecuting)
目前我没有可用的示例,我必须去,但我认为它是可行的
示例代码
public class MyDbInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
if (is your table)
{
command.CommandText = "Set Identity off ,update insert into ,Set Identity off"
return;
}
base.ReaderExecuting(command, interceptionContext);
}
}
ORM 是一个很好的抽象,我真的很喜欢它们,但我认为尝试 "hack" 它们来支持较低(更接近数据库)级别的操作没有意义。
我尽量避免存储过程,但我认为在这种情况下(正如你所说的例外)我认为你应该使用 one
我有一个非常相似的问题。
解决方案是这样的:
db.Database.ExecuteSqlCommand("disable trigger all on myTable ;")
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable OFF");
db.Database.ExecuteSqlCommand("enable trigger all on myTable ;")
在我的例子中,消息 Explicit value must be specified for identity...
是因为在插入时调用了一个触发器并会插入其他内容。
ALTER TABLE myTable NOCHECK CONSTRAINT all
也很有用
答案适用于 Entity Framework 6 只需使用 IDENTITY_INSERT 外部交易
using (var db = new AppDbContext())
{
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
using (var transaction = db .Database.BeginTransaction())
{
var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
db.IdentityItems.Add(item);
db.SaveChanges();
transaction.Commit();
}
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
}
我遇到了类似的问题。在我的生产代码中,实体依赖于身份生成。但是对于集成测试,我需要手动设置一些 ID。在我不需要明确设置它们的地方,我在 test data builders 中生成了它们。为了实现这一点,我创建了一个 DbContext
继承我的生产代码中的那个,并为每个实体配置身份生成,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Entity1>().Property(e => e.Id).ValueGeneratedNever();
modelBuilder.Entity<Entity2>().Property(e => e.Id).ValueGeneratedNever();
...
}
但这还不够,我不得不禁用 SQL 服务器 IDENTITY_INSERT
。这在单个 table 中插入数据时有效。但是当你有彼此相关的实体并且你想插入一个对象图时,这在 DbContext.SaveChanges()
上失败了。原因是根据 SQL Server documentation you can have IDENTITY_INSERT ON
just for one table at a time during a session. My colleague suggested to use a DbCommandInterceptor
which is similar to the INSERT INTO
only but the concept could be expanded further. Currently it intercepts and modifies multiple INSERT INTO
statements within a single DbCommand.CommandText
. The code could be optimized to use Span.Slice 为了避免由于字符串操作而占用太多内存,但由于我找不到 Split
方法,所以我没有花时间在这上面。无论如何,我正在使用这个 DbCommandInterceptor
进行集成测试。觉得对您有帮助,欢迎采纳。
/// <summary>
/// When enabled intercepts each INSERT INTO statement and detects which table is being inserted into, if any.
/// Then adds the "SET IDENTITY_INSERT table ON;" (and same for OFF) statement before (and after) the actual insertion.
/// </summary>
public class IdentityInsertInterceptor : DbCommandInterceptor
{
public bool IsEnabled { get; set; }
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
if (IsEnabled)
{
ModifyAllStatements(command);
}
return base.ReaderExecuting(command, eventData, result);
}
private static void ModifyAllStatements(DbCommand command)
{
string[] statements = command.CommandText.Split(';', StringSplitOptions.RemoveEmptyEntries);
var commandTextBuilder = new StringBuilder(capacity: command.CommandText.Length * 2);
foreach (string statement in statements)
{
string modified = ModifyStatement(statement);
commandTextBuilder.Append(modified);
}
command.CommandText = commandTextBuilder.ToString();
}
private static string ModifyStatement(string statement)
{
const string insertIntoText = "INSERT INTO [";
int insertIntoIndex = statement.IndexOf(insertIntoText, StringComparison.InvariantCultureIgnoreCase);
if (insertIntoIndex < 0)
return $"{statement};";
int closingBracketIndex = statement.IndexOf("]", startIndex: insertIntoIndex, StringComparison.InvariantCultureIgnoreCase);
string tableName = statement.Substring(
startIndex: insertIntoIndex + insertIntoText.Length,
length: closingBracketIndex - insertIntoIndex - insertIntoText.Length);
// we should probably check whether the table is expected - list with allowed/disallowed tables
string modified = $"SET IDENTITY_INSERT [{tableName}] ON; {statement}; SET IDENTITY_INSERT [{tableName}] OFF;";
return modified;
}
}