我如何强制entity framework插入标识列?
我想写一些C#代码来初始化我的数据库与一些种子数据。 显然,这将需要能够在插入时设置各种标识列的值。 我使用的是代码优先的方法。 默认情况下, DbContext
处理数据库连接,因此您不能SET IDENTITY_INSERT [dbo].[MyTable] ON
。 所以,到目前为止,我所做的是使用DbContext
构造函数来指定要使用的数据库连接。 然后,我在该数据库连接中将IDENTITY_INSERT
设置为ON
,然后尝试使用entity framework插入我的logging。 以下是我迄今为止的一个例子:
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()
时,entity framework不会尝试设置它,即使我给MyUser
实体一个MyUser
的值属性。
有没有办法强制entity framework尝试甚至插入实体的主键值呢? 或者,也许暂时将MyUserId
标记为不是主键值的方法,EF会尝试插入它?
EF 6的方法,使用MSDN文章 :
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属性的值模型devise器中从Identity到None的标识列。
请注意,将StoreGeneratedPattern更改为None将无法插入没有指定id的对象(正常方式),错误为“IDENTITY_INSERT设置为OFF时无法在表'TableName'中为标识列插入显式值”。
你不需要通过连接做任何有趣的事情,你可以剪掉中间人,只是使用ObjectContext.ExecuteStoreCommand
。
你可以通过这样做来达到你想要的:
context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON");
我不知道任何告诉EF设置身份插入的内置方式。
这不是完美的,但它会比目前的方法更灵活,更“黑客”。
更新:
我刚刚意识到,你的问题还有第二部分。 现在你已经告诉SQL你想要做身份插入,EF甚至不会为所说的身份插入值(为什么呢?我们没有告诉它)。
我还没有使用代码优先的经验,但是从一些快速search看来,您需要告诉EF您的列不应该从商店生成。 你需要做这样的事情。
Property(obj => obj.MyUserId) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) .HasColumnName("MyUserId");
希望这会让你指出正确的方向:-)
比赛晚了,但是如果有人在使用DB的EF5中遇到这个问题,我不能得到任何解决scheme的工作,但发现另一个解决方法:
在运行.SaveChanges()
命令之前,我重置了表的标识计数器:
Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1);); Entities.YourTable.Add(newObject); Entities.SaveChanges();
这意味着.SaveChanges()
需要在每次添加后应用 – 但至less它是有效的!
仔细考虑后,我决定entity framework拒绝插入标识列是一个function,而不是一个错误。 :-)如果我要插入我的数据库中的所有条目,包括他们的身份值,我也必须创build一个实体为每个链接表,entity framework已经为我自动创build! 这只是不正确的做法。
所以我在做的是设置种子类,只使用C#代码并创buildEF实体,然后使用DbContext
来保存新创build的数据。 将转储的SQL转换成C#代码需要花费更长的时间,但是没有(也不应该是)仅仅为了“播种”数据而使用太多的数据 – 它应该是一小部分具有代表性的数据这些数据可以放在一个实时数据库中,这些数据可以快速放入一个新的数据库中用于debugging/开发目的。 这确实意味着如果我想连接实体在一起,我不得不对已经插入的内容进行查询,或者我的代码不知道它们生成的标识值,例如。 这种事情会出现在种子代码中,我已经build立和完成context.SaveChanges
for 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 – > SQL Server,PostgreSQL – > PostgreSQL等)中从一个数据库复制到另一个数据库时,然后执行它在一个SQL脚本中,而不是EF代码优先(SQL脚本不会与数据库无关,但不需要;不在不同的DBMS之间)。
我只是一个DBA,但是当这样的事情popup时,我认为它是一种代码味道。 也就是说,为什么你有什么依赖某些行具有一定的身份值? 也就是说,在上面的例子中,为什么诺维特太太需要身份值106? 而不是依靠这种情况,你可以得到她的身份价值和使用,无论你硬编码106.更麻烦一些,但方式更灵活(在我的意见)。
这个想法只有在目标表为空的情况下才能可靠地工作,或者将logging插入的表的id高于表中所有已经存在的id!
3年后,我遇到了将生产数据转换为testing系统的类似问题。 用户希望能够随时将生产数据复制到testing系统中,所以不用在SQL Server中设置传输作业,而是使用现有的EF类来完成应用程序中的传输。 这样我可以提供给用户一个菜单项,以便随时开始传输。
该应用程序使用MS SQL Server 2008数据库和EF 6.由于这两个数据库通常具有相同的结构,我想我可以很容易地从一个DbContext实例传输数据到另一个阅读每个实体使用AsNoTracking()
的logging,只是Add()
(或AddRange()
)logging到目标DbContext实例的相应属性。
这里是一个DbContext与一个实体来说明:
public class MyDataContext: DbContext { public virtual DbSet<Person> People { get; set; } }
要复制人员数据,我做了以下操作:
private void CopyPeople() { var records = _sourceContext.People.AsNoTracking().ToArray(); _targetContext.People.AddRange(records); _targetContext.SaveChanges(); }
只要表格按照正确的顺序复制(为了避免外键约束的问题),这个工作非常好。 不幸的是,使用标识列的表使事情变得有点困难,因为EF忽略了id值,只是让SQL Server插入下一个标识值。 对于具有标识列的表,我最终执行以下操作:
- 阅读给定实体的所有logging
- 按升序排列logging
- 将表格的标识种子设置为第一个标识的值
- 跟踪下一个身份值,逐个添加logging。 如果id与预期的下一个标识值不同,则将标识种子设置为下一个所需的值
只要表是空的(或者所有新logging的ID都高于当前最高ID),并且ID按升序排列,EF和MS SQL将插入所需的ID,这两个系统都不会抱怨。
下面是一些代码来说明:
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(); }
使用reflection,我能够编写一个Load
和Save
方法,为DbContext中的所有实体工作。
这是一个黑客,但它允许我使用标准的EF方法来读写实体,并克服了如何在一组给定的情况下将标识列设置为特定值的问题。
我希望这会对其他面临类似问题的人有所帮助。
这是问题的解决scheme。 我已经在EF6上试了一下,它为我工作。 以下是一些应该工作的伪代码。
首先你需要创build默认的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; }
并在模型创build删除数据库生成的选项,
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(); }
试验了这个网站上的几个选项后,下面的代码为我工作( EF 6 )。 注意,如果项目已经存在,它首先尝试正常更新。 如果不是,则尝试正常插入,如果错误是由IDENTITY_INSERT导致的,则尝试解决方法。 还要注意db.SaveChanges将失败,因此db.Database.Connection.Open()语句和可选的validation步骤。 请注意这不是更新上下文,但在我的情况下,这是没有必要的。 希望这可以帮助!
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; }
有没有办法强制entity framework尝试甚至插入实体的主键值?
是的,但并不像我希望看到的那么干净。
假设您使用自动生成的身份密钥,则EF将完全忽略您存储密钥值的尝试。 由于上面详述的许多原因,这似乎是“按devise”,但是仍然有时候要完全控制种子数据(或初始负载)。 我build议EF在未来的版本中适合这种播种。 但是,直到他们这样做,只要编写一个在框架内工作的小代码,就可以自动处理这些混乱的细节。
Eventho供应商ID被EF忽略,您可以将其与基本循环和计数一起使用,以确定在您的实时logging之间添加多less个占位符logging。 占位符被添加时被分配下一个可用的ID号码。 一旦你的实时logging有请求的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的顺序。
我无法find将logging插入到表中的方法。 基本上,我创build了一个像这样的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。