【问题标题】:IDENTITY_INSERT during seeding with EntityFramework 6 Code-First使用 EntityFramework 6 Code-First 播种期间的 IDENTITY_INSERT
【发布时间】:2014-08-07 13:17:18
【问题描述】:

我有一个具有Auto-identity (int) 列的实体。作为数据种子的一部分,我想在我的系统中为“标准数据”使用特定的标识符值,之后我想让数据库整理出 id 值。

到目前为止,我已经能够将 IDENTITY_INSERT 设置为 On 作为插入批处理的一部分,但 Entity Framework 不会生成包含 Id 的插入语句。这是有道理的,因为模型认为数据库应该提供值,但在这种情况下,我想提供值。

模型(伪代码):

public class ReferenceThing
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id{get;set;}
    public string Name{get;set;}
}

public class Seeder
{
    public void Seed (DbContext context)
    {

        var myThing = new ReferenceThing
        {
            Id = 1,
            Name = "Thing with Id 1"
        };

        context.Set<ReferenceThing>.Add(myThing);

        context.Database.Connection.Open();
        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON")

        context.SaveChanges();  // <-- generates SQL INSERT statement
                                //     but without Id column value

        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF")
    }
}

谁能提供任何见解或建议?

【问题讨论】:

  • 通常当我在我的代码中看到这样的东西时,我会觉得设计有问题。如果Id 不是一个身份,则不要将其设为一个身份...如果您需要表中ID 为1 的内容,您可能需要为标识符添加一个额外的列。通常,我的查找表有一个 ID、一个用于内部引用的名称和一个显示在 UI 中的描述。
  • @Tallmaris 好点,我也有同感,但对“超级”用户有一个模糊的要求,即能够编写跨多个安装运行的 SQL 报告,因此要保留报告代码更简单的是,我们决定尝试已知的 Id 值。我这里几乎可以开一罐虫子了……

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


【解决方案1】:

如果没有第二个 EF 级别模型,则无法完成 - 复制用于播种的类。

正如您所说 - 您的元数据表明数据库提供了值,而在播种期间它不提​​供。

【讨论】:

  • 认为可能是这样。虽然我读到不可能在同一个基础表上拥有两个实体 (stackoverflow.com/questions/5093563/…)
  • 是的。通常,我远离通过 EF 进行数据加载的原因之一。无论如何,EF 对批量操作的性能非常糟糕。
【解决方案2】:

所以我可能已经解决了这个问题,方法是生成我自己的包含 Id 列的 SQL 插入语句。感觉就像一个可怕的 hack,但它确实有效:-/

public class Seeder
{
    public void Seed (DbContext context)
    {

        var myThing = new ReferenceThing
        {
            Id = 1,
            Name = "Thing with Id 1"
        };

        context.Set<ReferenceThing>.Add(myThing);

        context.Database.Connection.Open();
        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON")

        // manually generate SQL & execute
        context.Database.ExecuteSqlCommand("INSERT ReferenceThing (Id, Name) " +
                                           "VALUES (@0, @1)", 
                                           myThing.Id, myThing.Name);

        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF")
    }
}

【讨论】:

  • 我会这样做。 (嗯,你答案的后半部分,前半部分不需要)
  • @RikRak 我应该传递什么而不是 @0, @1 因为它给了我一个错误。
  • 如果我使用“@p0,@p1”而不是“@0,@1”对我有用。 (注意:引用位置只是因为 StackOverflow 认为我在标记用户。)
【解决方案3】:

如果您使用数据库优先模型,则应将 ID 列的 StoreGeneratedPattern 属性从 Identity 更改为 None

在那之后,正如我回复here,这应该会有所帮助:

using (var transaction = context.Database.BeginTransaction())
{
    var myThing = new ReferenceThing
    {
        Id = 1,
        Name = "Thing with Id 1"
    };

    context.Set<ReferenceThing>.Add(myThing);

    context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON");

    context.SaveChanges();

    context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF");

    transaction.Commit();
}

【讨论】:

    【解决方案4】:

    我为我的DbContext 创建了一个备用构造函数,它接受一个布尔值allowIdentityInserts。我将该布尔值设置为DbContext 上同名的私有字段。

    如果我在该“模式”下创建上下文,我的 OnModelCreating 然后“取消指定”身份规范

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    
            if(allowIdentityInsert)
            {
                modelBuilder.Entity<ChargeType>()
                    .Property(x => x.Id)
                    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
            }
        }
    

    这允许我在不更改实际数据库身份规范的情况下插入 Id。我仍然需要使用您所做的身份插入开/关技巧,但至少 EF 会发送 Id 值。

    【讨论】:

    • 我认为使用 HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) 的问题在于,当您随后将应用程序与实体框架一起使用时,插入将失败,因为 EF 现在期望任何插入都提供 ID。
    • @Gwasshoppa 当我使用我的应用程序时,上下文是使用将 allowIdentityInsert 设为“false”的构造函数创建的,因此应用程序按预期工作。
    • 当我使用自定义构造函数实例化我的 dbcontext 时,我的 OnModelCreating() 永远不会被调用。我错过了什么吗?
    • @Chris 这似乎不起作用,因为迁移认为模型与数据库不同步。你是怎么解决的?另外,您如何让迁移使用您的自定义构造函数?
    • @Chris 我添加了一些日志以查看发生了什么,并且确实在我的构造函数有机会设置它的值之前调用了OnModelCreating() 。我不知道他们是怎么做到的。无论如何,我们创建了第二个派生的DbContext,它允许身份插入,并在需要插入这些列的特殊情况下使用它。有关详细信息,请参阅下面的答案。
    【解决方案5】:

    在尝试了在此站点上找到的几个选项后,以下代码对我有用 (EF 6)。请注意,如果项目已经存在,它首先会尝试正常更新。如果没有,则尝试正常插入,如果错误是由 IDENTITY_INSERT 引起的,则尝试解决方法。还要注意 db.SaveChanges 将失败,因此 db.Database.Connection.Open() 语句和可选的验证步骤。请注意,这不会更新上下文,但在我的情况下,这不是必需的。希望这会有所帮助!

    public static bool UpdateLeadTime(int ltId, int ltDays)
    {
        try
        {
            using (var db = new LeadTimeContext())
            {
                var result = db.LeadTimes.SingleOrDefault(l => l.LeadTimeId == ltId);
    
                if (result != null)
                {
                    result.LeadTimeDays = ltDays;
                    db.SaveChanges();
                    logger.Info("Updated ltId: {0} with ltDays: {1}.", ltId, ltDays);
                }
                else
                {
                    LeadTime leadtime = new LeadTime();
                    leadtime.LeadTimeId = ltId;
                    leadtime.LeadTimeDays = ltDays;
    
                    try
                    {
                        db.LeadTimes.Add(leadtime);
                        db.SaveChanges();
                        logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                    }
                    catch (Exception ex)
                    {
                        logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex.Message);
                        logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
                        if (ex.InnerException.InnerException.Message.Contains("IDENTITY_INSERT"))
                        {
                            logger.Warn("Attempting workaround...");
                            try
                            {
                                db.Database.Connection.Open();  // required to update database without db.SaveChanges()
                                db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] ON");
                                db.Database.ExecuteSqlCommand(
                                    String.Format("INSERT INTO[dbo].[LeadTime]([LeadTimeId],[LeadTimeDays]) VALUES({0},{1})", ltId, ltDays)
                                    );
                                db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] OFF");
                                logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                                // No need to save changes, the database has been updated.
                                //db.SaveChanges(); <-- causes error
    
                            }
                            catch (Exception ex1)
                            {
                                logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex1.Message);
                                logger.Warn("Inner exception message: {0}", ex1.InnerException.InnerException.Message);
                            }
                            finally
                            {
                                db.Database.Connection.Close();
                                //Verification
                                if (ReadLeadTime(ltId) == ltDays)
                                {
                                    logger.Info("Insertion verified. Workaround succeeded.");
                                }
                                else
                                {
                                    logger.Info("Error!: Insert not verified. Workaround failed.");
                                }
                            }
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            logger.Warn("Error in UpdateLeadTime({0},{1}) was caught: {2}.", ltId.ToString(), ltDays.ToString(), ex.Message);
            logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
            Console.WriteLine(ex.Message);
            return false;
        }
        return true;
    }
    

    【讨论】:

      【解决方案6】:

      尝试将此代码添加到您的数据库上下文中“以保持清洁”以便说话:

      使用场景示例(将 ID 0 默认记录添加到实体类型 ABCStatus:

      protected override void Seed(DBContextIMD context)
      {
          bool HasDefaultRecord;
          HasDefaultRecord = false;
          DBContext.ABCStatusList.Where(DBEntity => DBEntity.ID == 0).ToList().ForEach(DBEntity =>
          {
              DBEntity.ABCStatusCode = @"Default";
              HasDefaultRecord = true;
          });
          if (HasDefaultRecord) { DBContext.SaveChanges(); }
          else {
              using (var dbContextTransaction = DBContext.Database.BeginTransaction()) {
                  try
                  {
                      DBContext.IdentityInsert<ABCStatus>(true);
                      DBContext.ABCStatusList.Add(new ABCStatus() { ID = 0, ABCStatusCode = @"Default" });
                      DBContext.SaveChanges();
                      DBContext.IdentityInsert<ABCStatus>(false);
                      dbContextTransaction.Commit();
                  }
                  catch (Exception ex)
                  {
                      // Log Exception using whatever framework
                      Debug.WriteLine(@"Insert default record for ABCStatus failed");
                      Debug.WriteLine(ex.ToString());
                      dbContextTransaction.Rollback();
                      DBContext.RollBack();
                  }
              }
          }
      }
      

      为 Get Table Name 扩展方法添加这个帮助类

      public static class ContextExtensions
      {
          public static string GetTableName<T>(this DbContext context) where T : class
          {
              ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
      
              return objectContext.GetTableName<T>();
          }
      
          public static string GetTableName<T>(this ObjectContext context) where T : class
          {
              string sql = context.CreateObjectSet<T>().ToTraceString();
              Regex regex = new Regex(@"FROM\s+(?<table>.+)\s+AS");
              Match match = regex.Match(sql);
      
              string table = match.Groups["table"].Value;
              return table;
          }
      }
      

      添加到 DBContext 的代码:

      public MyDBContext(bool _EnableIdentityInsert)
          : base("name=ConnectionString")
      {
          EnableIdentityInsert = _EnableIdentityInsert;
      }
      
      private bool EnableIdentityInsert = false;
      
      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
              Database.SetInitializer(new MigrateDatabaseToLatestVersion<DBContextIMD, Configuration>());
              //modelBuilder.Entity<SomeEntity>()
              //    .Property(e => e.SomeProperty)
              //    .IsUnicode(false);
      
              // Etc... Configure your model
              // Then add the following bit
          if (EnableIdentityInsert)
          {
              modelBuilder.Entity<SomeEntity>()
                  .Property(x => x.ID)
                  .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
              modelBuilder.Entity<AnotherEntity>()
                  .Property(x => x.ID)
                  .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
          }
      }
      
      //Add this for Identity Insert
      
      /// <summary>
      /// Enable Identity insert for specified entity type.
      /// Note you should wrap the identity insert on, the insert and the identity insert off in a transaction
      /// </summary>
      /// <typeparam name="T">Entity Type</typeparam>
      /// <param name="On">If true sets identity insert on else set identity insert off</param>
      public void IdentityInsert<T>(bool On)
          where T: class
      {
          if (!EnableIdentityInsert)
          {
              throw new NotSupportedException(string.Concat(@"Cannot Enable entity insert on ", typeof(T).FullName, @" when _EnableIdentityInsert Parameter is not enabled in constructor"));
          }
          if (On)
          {
              Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" ON"));
          }
          else
          {
              Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" OFF"));
          }
      }
      
      //Add this for Rollback changes
      
      /// <summary>
      /// Rolls back pending changes in all changed entities within the DB Context
      /// </summary>
      public void RollBack()
      {
          var changedEntries = ChangeTracker.Entries()
              .Where(x => x.State != EntityState.Unchanged).ToList();
      
          foreach (var entry in changedEntries)
          {
              switch (entry.State)
              {
                  case EntityState.Modified:
                      entry.CurrentValues.SetValues(entry.OriginalValues);
                      entry.State = EntityState.Unchanged;
                      break;
                  case EntityState.Added:
                      entry.State = EntityState.Detached;
                      break;
                  case EntityState.Deleted:
                      entry.State = EntityState.Unchanged;
                      break;
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        根据之前的Question,您需要开始您的上下文事务。保存更改后,您还必须重新声明 Identity Insert 列,最后您必须提交事务。

        using (var transaction = context.Database.BeginTransaction())
        {
            var item = new ReferenceThing{Id = 418, Name = "Abrahadabra" };
            context.IdentityItems.Add(item);
            context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
            context.SaveChanges();
            context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");
            transaction.Commit();
        }
        

        【讨论】:

          【解决方案8】:

          对于未来的 Google 员工,我发现在 OnModelCreating() 中暗示某些条件逻辑的答案对我不起作用。

          这种方法的主要问题是 EF 缓存模型,因此无法在同一个应用程序域中打开或关闭身份。

          我们采用的解决方案是创建第二个派生DbContext,它允许身份插入。这样,两个模型都可以被缓存,并且您可以在需要插入标识值的特殊(并且希望是)极少数情况下使用派生的DbContext

          鉴于@RikRak 的问题如下:

          public class ReferenceThing
          {
              [Key]
              [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
              public int Id { get; set; }
              public string Name { get; set; }
          }
          
          public class MyDbContext : DbContext 
          {
              public DbSet<ReferenceThing> ReferenceThing { get; set; }   
          }
          

          我们添加了这个派生的DbContext

          public class MyDbContextWhichAllowsIdentityInsert : MyDbContext
          {
              protected override void OnModelCreating(DbModelBuilder modelBuilder)
              {
                  base.OnModelCreating(modelBuilder);
          
                  modelBuilder.Entity<ReferenceThing>()
                              .Property(x => x.Id)
                              .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
              }
          }
          

          然后将与Seeder 一起使用,如下所示:

          var specialDbContext = new MyDbContextWhichAllowsIdentityInsert();
          
          Seeder.Seed(specialDbContext);
          

          【讨论】:

            【解决方案9】:

            假设您有一个名为 Branch 的表,其中有一列名为 BranchId 的整数类型。 按照 SQL Server 的约定,EF 将假定整数类型的列是标识列。

            因此它会自动将列的身份规范设置为:

            • (是身份)是的
            • 标识增量 1
            • 身份种子1

            如果您想为实体分配 id 值,请使用 DatabaseGeneratedOption,如下所示:

            public class Branch
            {
                [Key]
                [DatabaseGenerated(DatabaseGeneratedOption.None)]
                public int BranchId { get; set; }
                public string Description { get; set; }
            }
            

            然后您可以播种数据并将您想要的任何值分配给 BranchId

            【讨论】:

              猜你喜欢
              • 2012-01-16
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-09-12
              • 1970-01-01
              相关资源
              最近更新 更多