为什么这个带有IDENTITY_INSERT的EF插入不起作用?

时间:2017-02-02 06:23:58

标签: c# entity-framework entity-framework-6 identity-insert

这是查询:

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仍为1.

新:当我使用交易或TGlatzer的答案时,我得到例外:

  

必须为表格中的标识列指定显式值'项目'   当IDENTITY_INSERT设置为ON或复制用户时   插入NOT FOR REPLICATION标识列。

8 个答案:

答案 0 :(得分:8)

根据之前的Question,您需要开始上下文的交易。保存更改后,您还必须重新标识“身份插入”列,最后必须提交事务。

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();
}

答案 1 :(得分:3)

我没有尊重问题的标签,告诉这是关于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将绑定到您的连接。

答案 2 :(得分:3)

要强制EF写入您实体的ID,您必须将ID配置为不存储生成,否则EF将永远不会在插入语句中包含该ID。

因此,您需要动态更改模型并根据需要配置实体ID 问题是模型是缓存的,并且在运行中更改它是非常棘手的(我很确定我已经完成了它但实际上我找不到代码,可能我把它扔掉了)。最简单的方法是创建两个不同的上下文,您可以通过两种不同的方式配置实体,如(x+x)(当您需要编写ID时)和DatabaseGeneratedOption.None(当您需要自动编号ID时)。

答案 3 :(得分:1)

绝不能在生产代码中使用它,只是为了好玩

我不建议这样做,因为这是一个疯狂的黑客,但无论如何。

我认为我们可以通过拦截sql命令并更改命令文本来实现它  (您可以继承自DbCommandInterceptor和adver ovveride ReaderExecuting)

我现在没有一个有效的例子,我必须去,但我认为这是可行的

示例代码

    public class MyDbInterceptor : DbCommandInterceptor
    {
        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {

            if (is your table)
            {
                command.CommandText = "Set Identoty off ,update insert into ,Set Identity off"
                return;
            }
            base.ReaderExecuting(command, interceptionContext);

        }

    }

ORM是一个很好的抽象,我真的很喜欢它们,但我认为尝试&#34; hack&#34;是不合理的。它们支持更低(更接近数据库)级别的操作 我试图避免存储过程,但我认为在这种情况下(如你所说的例外)我认为你应该使用一个

答案 4 :(得分:1)

我有一个类似的问题。在我的生产代码中,实体依赖于身份生成。但是对于集成测试,我需要手动设置一些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 Server IDENTITY_INSERT。在单个表中插入数据时,此方法有效。但是,当您拥有彼此相关的实体并且想要插入对象图时,此操作将在DbContext.SaveChanges()上失败。原因是,根据SQL Server documentation,您可以在会话期间一次只为一张表使用IDENTITY_INSERT ON。我的同事建议使用与other answer to this question类似的DbCommandInterceptor。我仅将其用于INSERT INTO,但可以进一步扩展该概念。当前,它在单个INSERT INTO中拦截并修改多个DbCommand.CommandText语句。可以对代码进行优化以使用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;
    }
}

答案 5 :(得分:0)

即使您关闭IDENTITY_INSERT,您刚刚告诉SQL我将向您发送身份,您没有告诉实体框架将身份发送到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();
    }
}

答案 6 :(得分:0)

我有一个非常类似的问题。

解决方案如下:

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

也可以是有用的

答案 7 :(得分:0)

答案适用于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");
}