【问题标题】:Why does this EF insert with IDENTITY_INSERT not work?为什么这个带有 IDENTITY_INSERT 的 EF 插入不起作用?
【发布时间】:2017-06-19 03:03:42
【问题描述】:

这是查询:

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 标识列。

【问题讨论】:

  • 根据这个答案Entity Framework IDENTITY_INSERT ON doesn't work,您只是短暂地打开了身份插入,它不包括插入。如果可能,请尝试创建一个大的 sql 查询字符串,以 "SET IDENTITY_INSERT Test.Items ON;" 开头,然后是 INSERT INTO...,最后是 "SET IDENTITY_INSERT Test.Items OFF;"(多合一查询)。或者,您可以查看TransactionScope Class
  • @KeyurPATEL 使用直接的INSERT 查询确实违背了 ORM 的目的。我更喜欢交易选项,但也更喜欢内置交易,就像 Aananda 的回答一样。
  • @ProfK : 对于例外情况,您能否到模型设计器(edmx)并选择属性 Id(这是 Identity 列)并将 StoreGenratedPattern 修改为 None 而不是 Identity ?您必须更改它,因为您要插入标识列。这应该可以修复异常。
  • 如果没有任何效果,一种选择是映射存储过程以插入该实体。 entityframeworktutorial.net/entityframework6/… 。您可以传递一个标志,该标志将根据标志值设置识别插入。
  • @GertArnold 虽然主要问题不是如何维护连接,而是如何让 EF 在插入命令中包含 Id 值。

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


【解决方案1】:

根据之前的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();
}

【讨论】:

    【解决方案2】:

    我不尊重关于 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 将绑定到您的连接。

    【讨论】:

    • 我在尝试这个时遇到了一个异常,并且还有另一个答案。有关详细信息,请参阅我对 OP 的编辑。
    • 糟糕。我以某种方式假设 EF Core .. 我的代码在 EF Core 中有效:) - 或者在您编辑之前一直有效
    • 哈哈,一块石头,一块坚硬的地方。由我更改的代码在 EF 6 上为我编译和运行,但仍然没有达到预期的效果。
    • 对于 EF Core 2,此代码确实有效。在 ExecuteSqlCommand 之前使用 OpenConnection 保持连接打开并且可以进行身份​​插入。
    【解决方案3】:

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

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

    【讨论】:

    • 但即便如此,每次我使用两种上下文中的不同一种时,我都必须进行迁移。如果该列是Identity 列,并且我将上下文与DatabaseGeneratedOption.None 一起使用,那么当我尝试将值插入该列时,我会从数据库中得到一个异常。我认为最好将存储过程用于我需要身​​份插入的非常罕见的事件。
    • 几次尝试后,我也停止使用 EF 迁移数据内容。我使用 ADO.Net 进行数据迁移。如果您不需要针对不同的 DBMS,存储过程是最快(最好)的方式
    • 在覆盖 DbContext 的 OnModelCreating 方法时忘记的代码可能是 modelBuilder.Conventions.Add<System.Data.Entity.ModelConfiguration.Conventions.StoreGeneratedIdentityKeyConvention>();
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      我遇到了类似的问题。在我的生产代码中,实体依赖于身份生成。但是对于集成测试,我需要手动设置一些 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 questionDbCommandInterceptor。我使它仅适用于INSERT INTO,但这个概念可以进一步扩展。目前,它拦截并修改单个DbCommand.CommandText 中的多个INSERT INTO 语句。可以优化代码以使用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;
          }
      }
      
      

      【讨论】:

      • 请注意,EF 有时依赖于插入项目的数量(即使用@@ROWCOUNT)。这些查询将不起作用,因为在调用 SET IDENTITY_INSERT [{tableName}] OFF;@@ROWCOUNT 设置为 0@
      【解决方案6】:

      绝对不能在生产代码中使用它,它只是为了好玩
      我看到我的仍然是公认的答案,再次,不要使用它(解决这个问题问题),检查下面的其他答案

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

      我想我们可以通过截取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 是一个很好的抽象,我真的很喜欢它们,但我认为尝试“破解”它们以支持较低(更接近 db)级别的​​操作是没有意义的。
      我尽量避免存储过程,但我认为在这种情况下(正如你所说的例外)我认为你应该使用一个

      【讨论】:

      • 再想一想,我想我会删除这个。我不认为你可以从这里获得你需要的 ID...
      • 嗯,你可以在你的上下文中添加一个属性,然后从interceptionContext.DbContexts访问它。我不会删除答案,但我希望有更好的解决方案......
      • 我的技术人员喜欢拦截的想法,但存储过程可能是最简洁的答案。
      • 哦,我喜欢拦截器用于检查 SQL 查询的想法,而不是 SQL Profiler 的过度杀伤力。这样我什至可以跟踪记录查询。
      • 但这不是针对该实体的所有查询执行的吗?还是我错过了什么?在 cmets 的某个地方,我看到您需要关闭此行为设置标识 - 仅用于特殊任务。是的,命令拦截器对于记录查询非常有用;我根据配置中的一些标志通过拦截器进行日志记录,以便我可以控制它。
      【解决方案7】:

      即使你关闭IDENTITY_INSERT,你只是告诉SQL我会给你发Identity,你没有告诉实体框架发送Identity给SQL server。

      所以基本上,你必须创建如下所示的 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();
          }
      }
      

      【讨论】:

      • 我喜欢您在这里的想法,但您似乎还没有尝试过您的代码。重新配置 IdentityItem 会导致 EF 内部模型发生更改,因此查询会引发以下异常:System.InvalidOperationException: The model backing the 'AppDbContext' context has changed since the database was created.。而且您不能将HasDatabaseGeneratedOptionHasKey 联系起来,我必须像在您的代码中进行编辑一样。
      • @ProfK 现在怎么样?创建两个不同的上下文,这将导致每个上下文类型创建一次模型。对不起,我忘记了缓存。如果这不起作用,那么唯一的替代方法是创建两个单独的上下文。
      • 我认为这是一个可行的解决方案。无论如何,为特殊任务或应用程序的多个隔离提供专门的上下文并不是一个坏主意。
      【解决方案8】:

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

      解决方案类似于:

      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
      

      也很有用

      【讨论】:

        猜你喜欢
        • 2011-12-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-15
        • 1970-01-01
        • 2019-04-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多