为什么这个带有 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;
    }
}