如何强制实体框架插入标识列?

时间:2012-10-26 11:22:39

标签: asp.net sql-server database entity-framework ef-code-first

我想编写一些C#代码来使用一些种子数据初始化我的数据库。显然,这需要能够在插入时设置各种Identity列的值。我正在使用代码优先的方法。默认情况下,DbContext处理数据库连接,因此您无法SET IDENTITY_INSERT [dbo].[MyTable] ON。所以,到目前为止我所做的是使用DbContext构造函数,它允许我指定要使用的数据库连接。然后,我在该数据库连接中将IDENTITY_INSERT设置为ON,然后尝试使用实体框架插入我的记录。这是我到目前为止所得到的一个例子:

public class MyUserSeeder : IEntitySeeder {
    public void InitializeEntities(AssessmentSystemContext context, SqlConnection connection) {
        context.MyUsers.Add(new MyUser { MyUserId = 106, ConceptPersonId = 520476, Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginId = "520476", Password="28c923d21b68fdf129b46de949b9f7e0d03f6ced8e9404066f4f3a75e115147489c9f68195c2128e320ca9018cd711df", IsEnabled = true, SpecialRequirements = null });
        try {
            connection.Open();
            SqlCommand cmd = new SqlCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON", connection);
            int retVal = cmd.ExecuteNonQuery();
            context.SaveChanges();
        }
        finally {
            connection.Close();
        }
    }
}

如此接近但到目前为止 - 因为尽管cmd.ExecuteNonQuery()工作正常,但当我运行context.SaveChanges()时,我被告知“必须为表格'MyUser'中的标识列指定显式值当IDENTITY_INSERT设置为ON或复制用户插入NOT FOR REPLICATION标识列时。“

据推测,因为MyUserId(MyUser表中的Identity列)是主键,所以当我调用context.SaveChanges()时,实体框架不会尝试设置它,即使我给了{{1}实体MyUser属性的值。

有没有办法强制实体框架尝试为实体插入偶数主键值呢?或者也许暂时将MyUserId标记为不是主键值的方法,以便EF尝试插入它?

11 个答案:

答案 0 :(得分:45)

EF 6 方法,使用msdn article

    using (var dataContext = new DataModelContainer())
    using (var transaction = dataContext.Database.BeginTransaction())
    {
      var user = new User()
      {
        ID = id,
        Name = "John"
      };

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] ON");

      dataContext.User.Add(user);
      dataContext.SaveChanges();

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");

      transaction.Commit();
    }

更新:为避免错误“当IDENTITY_INSERT设置为ON或复制用户插入NOT FOR REPLICATION标识列时,必须为表'TableName'中的标识列指定显式值“,您应该将标识列的 StoreGeneratedPattern 属性的值从标识设计器中的更改为

注意,将StoreGeneratedPattern更改为None将无法插入没有指定id的对象(正常方式),并且错误“当IDENTITY_INSERT设置为OFF时,无法在表'TableName'中为标识列插入显式值”。

答案 1 :(得分:18)

你不需要对连接做任何有趣的事情,你可以剪掉中间人并使用ObjectContext.ExecuteStoreCommand

然后你可以通过这样做来实现你想要的目标:

context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON");

我不知道有任何内置的方式告诉EF设置身份插入。

它并不完美,但它比你目前的方法更灵活,更少“黑客”。

<强>更新

我刚刚意识到你的问题还有第二部分。既然你已经告诉SQL你想要进行身份插入,EF甚至都没有尝试为所述身份插入值(为什么会这样?我们还没告诉它)。

我没有任何使用代码优先方法的经验,但是从一些快速搜索中,您似乎需要告诉EF您的列不应该从商店生成。你需要做这样的事情。

Property(obj => obj.MyUserId)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
    .HasColumnName("MyUserId");

希望这会让你指出正确的方向: - )

答案 2 :(得分:11)

派对迟到了,但是如果有人在EF5中首先遇到这个问题:我无法找到任何解决方案,但找到了另一种解决方法:

在运行.SaveChanges()命令之前,我重置了表的身份计数器:

Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1););
Entities.YourTable.Add(newObject);
Entities.SaveChanges();

这意味着每次添加后都需要应用.SaveChanges() - 但至少它有效!

答案 3 :(得分:6)

以下是问题的解决方案。我在EF6上尝试过,它对我有用。以下是一些应该有效的伪代码。

首先,您需要创建默认dbcontext的重载。如果检查基类,您将找到传递现有dbConnection的基类。 请检查以下代码 -

public MyDbContext(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection = true)
    {
        //optional
        this.Configuration.ProxyCreationEnabled = true;
        this.Configuration.LazyLoadingEnabled = true;
        this.Database.CommandTimeout = 360;
    }

在On modelcreating中删除db generated选项,如

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyTable>()
            .Property(a => a.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        base.OnModelCreating(modelBuilder);
    }

现在在代码中,您需要显式传递连接对象,

using (var connection = new System.Data.SqlClient.SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionStringName"].ConnectionString))
        {
            connection.Open();
            using (var context = new MyDbContext(connection, true))
            {
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] ON");
                context.MyTable.AddRange(objectList);
                context.SaveChanges();
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] OFF");
            }

            connection.Close();
        }

答案 4 :(得分:2)

经过慎重考虑,我认为实体框架拒绝插入标识列是一个特征,而不是一个bug。 :-)如果我要在我的数据库中插入所有条目,包括它们的标识值,我还必须为实体框架为我自动创建的每个链接表创建一个实体!这不是正确的做法。

所以我正在做的是设置只使用C#代码并创建EF实体的种子类,然后使用DbContext来保存新创建的数据。获取转储的SQL并将其转换为C#代码需要更长的时间,但是没有(并且不应该)仅仅用于“播种”数据的太多数据 - 它应该是少量数据,这是代表性的实时数据库中可以快速放入新数据库进行调试/开发的数据类型。这确实意味着如果我想将实体链接在一起,我必须对已经插入的内容进行查询,或者我的代码不知道它们生成的标识值,例如。在为context.SaveChanges设置并完成MyRoles之后,这种事情会出现在播种代码中:

var roleBasic = context.MyRoles.Where(rl => rl.Name == "Basic").First();
var roleAdmin = context.MyRoles.Where(rl => rl.Name == "Admin").First();
var roleContentAuthor = context.MyRoles.Where(rl => rl.Name == "ContentAuthor").First();

MyUser thisUser = context.MyUsers.Add(new MyUser {
    Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginUsername = "naldred", Password="c1c966821b68fdf129c46de949b9f7e0d03f6cad8ea404066f4f3a75e11514748ac9f68695c2128e520ca0275cd711df", IsEnabled = true, SpecialRequirements = null
});
thisUser.Roles.Add(roleBasic);

这样做也会使我更改模式时更新种子数据的可能性,因为当我更改模式时,我可能会破坏种子代码(如果删除字段或实体,现有的种子代码,使用该字段/实体将无法编译)。使用用于进行种子设定的SQL脚本,情况并非如此,SQL脚本也不会与数据库无关。

所以我认为,如果您尝试设置实体的身份字段来进行数据库播种数据,那么您肯定采取了错误的方法。

如果我实际上是将一大堆数据从SQL Server拖到PostgreSQL(一个完整的实时数据库,而不仅仅是一些播种数据),我可以通过EF来实现,但是我想在两个上下文中打开同时,编写一些代码从源上下文中获取所有各种实体并将它们放入目标上下文,然后保存更改。

通常,唯一适合插入标识值的是当您从同一个DBMS(SQL Server - &gt; SQL Server,PostgreSQL - &gt; PostgreSQL等)中的一个数据库复制到另一个数据库时,然后你会在SQL脚本而不是EF代码中做到这一点(SQL脚本不会与数据库无关,但它不需要;你不会在不同的DBMS之间进行)。

答案 5 :(得分:2)

如果目标表为空,或者插入的记录的ID高于表中所有已存在的ID,则此想法才能正常工作!

3年后,我遇到了将生产数据转移到测试系统中的类似问题。用户希望能够随时将生产数据复制到测试系统中,因此我没有在SQL Server中设置传输作业,而是使用现有的EF类来寻找在应用程序中完成传输的方法。这样我就可以为用户提供一个菜单项,以便随时开始传输。

该应用程序使用MS SQL Server 2008数据库和EF 6.由于这两个数据库通常具有相同的结构,我认为通过使用{{1}读取每个实体的记录,我可以轻松地将数据从一个DbContext实例传输到另一个只有AsNoTracking()(或Add())记录到目标DbContext实例上的相应属性。

这是一个DbContext,其中有一个实体用于说明:

AddRange()

要复制人物数据,我执行了以下操作:

public class MyDataContext: DbContext
{
    public virtual DbSet<Person> People { get; set; }
}

只要以正确的顺序复制表(以避免外键约束的问题),这就非常有效。不幸的是,使用标识列的表使事情变得有点困难,因为EF忽略了id值,只是让SQL Server插入下一个标识值。对于具有标识列的表,我最终执行以下操作:

  1. 阅读给定实体的所有记录
  2. 按ID按升序排列记录
  3. 将表的标识种子设置为第一个id的值
  4. 跟踪下一个身份值,逐个添加记录。如果id与预期的下一个标识值不同,则将标识种子设置为下一个所需值
  5. 只要表是空的(或者所有新记录的id都高于当前最高的id),并且id按升序排列,EF和MS SQL将插入所需的ID,系统都不会抱怨。

    以下是一些代码来说明:

    private void CopyPeople()
    {
        var records = _sourceContext.People.AsNoTracking().ToArray();
        _targetContext.People.AddRange(records);
        _targetContext.SaveChanges();
    }
    

    使用反射我能够编写一个适用于DbContext中所有实体的private void InsertRecords(Person[] people) { // setup expected id - presumption: empty table therefore 1 int expectedId = 1; // now add all people in order of ascending id foreach(var person in people.OrderBy(p => p.PersonId)) { // if the current person doesn't have the expected next id // we need to reseed the identity column of the table if (person.PersonId != expectedId) { // we need to save changes before changing the seed value _targetContext.SaveChanges(); // change identity seed: set to one less than id //(SQL Server increments current value and inserts that) _targetContext.Database.ExecuteSqlCommand( String.Format("DBCC CHECKIDENT([Person], RESEED, {0}", person.PersonId - 1) ); // update the expected id to the new value expectedId = person.PersonId; } // now add the person _targetContext.People.Add(person); // bump up the expectedId to the next value // Assumption: increment interval is 1 expectedId++; } // now save any pending changes _targetContext.SaveChanges(); } Load方法。

    这有点像黑客攻击,但它允许我使用标准的EF方法来读写实体,并克服了在一组给定情况下如何将标识列设置为特定值的问题。

    我希望这会对遇到类似问题的其他人有所帮助。

答案 6 :(得分:1)

在尝试了本网站上的几个选项后,以下代码对我有用( 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;
}

答案 7 :(得分:0)

我通过创建继承的上下文来完成这项工作:

我与EF迁移的常规情况:

public class MyContext : DbContext
{
    public MyContext() : base("name=MyConnexionString")
    {...}

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // best way to know the table names from classes... 
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        ...
    }
}

我用来替代身份的替代上下文。

请勿为EF迁移注册此上下文 (我用它来从另一个数据库传输数据):

public class MyContextForTransfers : MyContext
{
    public MyContextForTransfers() : base()
    {
        // Basically tells the context to take the database as it is...
        Database.SetInitializer<MyContextForTransfers >(null);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
         // Tells the Context to include Isd in inserts
         modelBuilder.Conventions.Remove<StoreGeneratedIdentityKeyConvention>();
         base.OnModelCreating(modelBuilder);
    }
}

如何插入(错误管理已大大简化...):

public void Insert<D>(iEnumerable<D> items)
{
    using (var destinationDb = new MyContextForTransfers())
    {
        using (var transaction = destinationDb.Database.BeginTransaction())
        {
            try
            {
                destinationDb.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT [dbo].[{typeof(D).Name}] ON");
                destinationDb.Set<D>().AddRange(items);
                destinationDb.SaveChanges();
                destinationDb.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT [dbo].[{typeof(D).Name}] OFF");
                transaction.Commit();
             }
             catch
             {
                transaction.Rollback();
             }
         }
    }
}

使用“常规”上下文和配置,在进行任何事务之前检查迁移可能是一个好主意:

答案 8 :(得分:-1)

我只是一名DBA,但每当这样的东西弹出时,我认为这是代码味道。也就是说,为什么你有任何依赖某些具有某些身份值的行?也就是说,在上面的例子中,为什么Novelette夫人需要身份值为106?你可以获得她的身份价值而不是依赖于那种情况,你可以获得她的身份价值,并且无论你有硬编码的地方106都可以使用它。这有点麻烦,但更灵活(在我看来)。

答案 9 :(得分:-1)

  

有没有办法强制实体框架尝试为实体插入偶数主键值?

是的,但不像我希望看到的那么干净。

假设您使用的是自动生成的身份密钥,EF将完全忽略您存储密钥值的尝试。由于上面列举的许多好理由,这似乎是“按设计”,但仍有时候您想要完全控制种子数据(或初始负荷)。我建议EF在未来的版本中适应这种播种。但是直到他们这样做,只需编写一些在框架内工作的代码,并自动化混乱的细节。

EF忽略了Eventho VendorID,您可以将其与基本循环和计数一起使用,以确定在您的实时记录之间添加多少个占位符记录。占位符在添加时会分配下一个可用的ID号。一旦您的实时记录具有所请求的ID,您只需要删除垃圾。

public class NewsprintInitializer: DropCreateDatabaseIfModelChanges<NewsprintContext>
{
    protected override void Seed(NewsprintContext context)
    {
        var vendorSeed = new List<Vendor>
        {
            new Vendor { VendorID = 1, Name = "#1 Papier Masson / James McClaren" },
            new Vendor { VendorID = 5, Name = "#5 Abitibi-Price" },
            new Vendor { VendorID = 6, Name = "#6 Kruger Inc." },
            new Vendor { VendorID = 8, Name = "#8 Tembec" }
        };

        //  Add desired records AND Junk records for gaps in the IDs, because .VendorID is ignored on .Add
        int idx = 1;
        foreach (Vendor currentVendor in vendorSeed)
        {
            while (idx < currentVendor.VendorID)
            {
                context.Vendors.Add(new Vendor { Name = "**Junk**" });
                context.SaveChanges();
                idx++;
            }
            context.Vendors.Add(currentVendor);
            context.SaveChanges();
            idx++;
        }
        //  Cleanup (Query/Find and Remove/delete) the Junk records
        foreach (Vendor del in context.Vendors.Where(v => v.Name == "**Junk**"))
        {
            context.Vendors.Remove(del);
        }
        context.SaveChanges();

        // setup for other classes

    }
}

它按预期工作,但我必须经常“SaveChanges”以保持ID顺序。

答案 10 :(得分:-2)

我找不到将记录插入表格的方法。基本上,我用这样的东西创建了一个SQL脚本......

            sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] ON;");

            foreach(...)
            {
                var insert = string.Format("INSERT INTO [dbo].[tblCustomer]
                     ([ID],[GivenName],[FamilyName],[NINumber],[CustomerIdent],
                      [InputterID],[CompanyId],[Discriminator]) 
                      VALUES({0}, '{1}', '{2}', '{3}', '{4}', 2, 2, 'tblCustomer'); ", 
                          customerId, firstName, surname, nINumber, Guid.NewGuid());

            sb.Append(insert);
                ...
            }

            sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] OFF;");
            using (var sqlConnection = new SqlConnection(connectionString))
            {
                var svrConnection = new ServerConnection(sqlConnection);
                var server = new Server(svrConnection);
                server.ConnectionContext.ExecuteNonQuery(sb.ToString());
        }

我正在使用EF 6。