如何强制实体框架插入标识列?
Posted
技术标签:
【中文标题】如何强制实体框架插入标识列?【英文标题】:How can I force entity framework to insert identity columns? 【发布时间】:2012-10-16 15:57:18 【问题描述】:我想编写一些 C# 代码来用一些种子数据初始化我的数据库。显然,这将需要能够在插入时设置各种标识列的值。我正在使用代码优先的方法。默认情况下,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()
时,实体框架不会尝试设置它,即使我给了 MyUser
实体一个值对于MyUserId
属性。
那么有没有办法强制实体框架尝试插入实体的主键值呢?或者可能是一种暂时将MyUserId
标记为不是主键值的方法,以便 EF 尝试插入它?
【问题讨论】:
this 可能会有所帮助 @MichalKlouda 这个答案似乎是从数据库优先而不是代码优先的方法来处理的。 IDENTITY_INSERT ON not being respected for Entity Framework DBSet.Add method的可能重复 【参考方案1】: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 属性值从 Identity 更改为 None。
注意,将 StoreGeneratedPattern 更改为 None 将无法插入没有指定 id 的对象(正常方式),并出现错误“当 IDENTITY_INSERT 设置为 OFF 时,无法在表 'TableName' 中插入标识列的显式值”。
【讨论】:
这根本没有解决 EF 不插入密钥的问题——而是解决了如何使其成为事务性 好吧,它使它成为事务性的,但它解决了身份插入的问题,不是吗? 当我尝试这个时,我得到了错误:“当 IDENTITY_INSERT 设置为 ON 或复制用户插入 NOT FOR 时,必须为表 'myTable' 中的标识列指定显式值REPLICATION 标识列。” (即使在调用 SaveChanges 之前我确实为我的表记录对象的标识字段设置了一个值。) @RomanO 感谢您的帮助!但是,我检查了一下,我的身份列上的“Not For Replication”属性设置为 false。我怀疑问题在于 EF 没有尝试将我指定的 ID 值发送到数据库(如琼斯医生回答的“更新”中所述)。 似乎需要将其包装在事务中才能使其正常工作【参考方案2】:你不需要对连接做任何有趣的事情,你可以去掉中间人,直接使用ObjectContext.ExecuteStoreCommand
。
然后你可以通过这样做来实现你想要的:
context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON");
我不知道有任何内置方式告诉 EF 设置身份插入。
它并不完美,但它会比你当前的方法更灵活,更不“hacky”。
更新:
我刚刚意识到您的问题还有第二部分。既然您已经告诉 SQL 您想要进行身份插入,EF 甚至不会尝试为所述身份插入值(为什么会这样?我们没有告诉它这样做)。
我没有任何使用代码优先方法的经验,但从一些快速搜索看来,您似乎需要告诉 EF 您的列不应该从商店生成。你需要做这样的事情。
Property(obj => obj.MyUserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("MyUserId");
希望这会让你指向正确的方向:-)
【讨论】:
嗯,Property
代码必须在 OnModelCreating
中完成,不是吗?在OnModelCreating
被执行之后,就没有办法向 EF 指示该信息了吗?
大问题?如果它只是您最初运行应用程序时的一次性权利,则不会为现有数据播种。它对于以与 DB 无关的方式快速设置开发数据库也非常有用(当然您可以使用 TSQL 执行此操作,但您被锁定在 SQL Server 中)。
是的,我周五下午脑子有问题,因此我删除了我的评论。如果您找不到更好的方法,我仍然认为创建一个专门用于执行身份插入的实体是一种可能的选择。让我们知道你的想法,我很想知道!
您可以有条件地应用DatabaseGenerationOption。我explained how I did it in my answer.
我认为使用 HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) 的问题在于,当您随后将应用程序与实体框架一起使用时,插入将失败,因为 EF 现在期望任何插入都提供 ID。 【参考方案3】:
聚会有点晚了,但万一有人在 EF5 中首先使用 DB 遇到此问题:我无法让任一解决方案起作用,但找到了另一种解决方法:
在运行.SaveChanges()
命令之前,我重置了表的身份计数器:
Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, 0)", newObject.Id-1););
Entities.YourTable.Add(newObject);
Entities.SaveChanges();
这意味着每次添加后都需要应用.SaveChanges()
- 但至少它有效!
【讨论】:
嘿,您的解决方案适用于更新目的,但我现在遇到的一个问题是它只是开始使用主键 0 添加第一条记录。 节省时间,修改列的解决方案并不理想,因为您需要更新生产服务器两次。这很完美。请记住,当您知道要插入第一条记录时,请删除-1
。
为什么插入的第一行没有正确的 ID?
如果插入代码可能同时运行(例如,典型网站的一部分),请确保将 SQL 命令和行添加包装在事务中。否则,有时,您的应用程序会认为新对象具有一个 ID,而 SQL Server 将存储一个不同的 ID。调试很有趣!
使用事务支持built into EF或EF Core。【参考方案4】:
这是问题的解决方案。我已经在 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;
在创建模型时删除 db 生成的选项,例如,
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();
【讨论】:
这种方法在zeffron.wordpress.com/2016/06/03/… 中有详细描述【参考方案5】:这个想法只有在目标表为空,或者插入的记录的 id 高于表中所有现有 id 的情况下才有效!
3 年过去了,我在将生产数据传输到测试系统时遇到了类似的问题。用户希望能够随时将生产数据复制到测试系统中,因此我没有在 SQL Server 中设置传输作业,而是寻找一种使用现有 EF 类在应用程序中完成传输的方法。这样我就可以为用户提供一个菜单项,让他们可以随时开始传输。
该应用程序使用 MS SQL Server 2008 数据库和 EF 6。由于这两个数据库通常具有相同的结构,我认为我可以通过使用 AsNoTracking()
和读取每个实体的记录轻松地将数据从一个 DbContext 实例传输到另一个实例只需将 Add()
(或 AddRange()
)记录到目标 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 插入下一个标识值。对于具有标识列的表,我最终执行了以下操作:
-
读取给定实体的所有记录
按id升序排列记录
将表的标识种子设置为第一个 id 的值
跟踪下一个标识值,逐一添加记录。如果 id 与预期的下一个身份值不同,则将身份种子设置为下一个所需值
只要表是空的(或者所有新记录的 id 都高于当前的 hisghest 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();
使用反射,我能够编写适用于 DbContext 中所有实体的 Load
和 Save
方法。
这有点小技巧,但它允许我使用标准 EF 方法来读取和写入实体,并解决了如何在一组给定情况下将标识列设置为特定值的问题。
我希望这对面临类似问题的其他人有所帮助。
【讨论】:
我认为格式需要是$"DBCC CHECKIDENT ('tableName', RESEED, actualId - 1);"
【参考方案6】:
经过仔细考虑,我认为实体框架拒绝插入标识列是一个特性,而不是一个错误。 :-) 如果我要在我的数据库中插入所有条目,包括它们的标识值,我还必须为实体框架为我自动创建的每个链接表创建一个实体!这不是正确的方法。
所以我要做的是设置只使用 C# 代码并创建 EF 实体的种子类,然后使用 DbContext
来保存新创建的数据。将转储的 SQL 转换为 C# 代码需要更长的时间,但没有(也不应该)过多的数据仅用于“播种”数据 - 它应该是具有代表性的少量数据可以在实时数据库中快速放入新数据库以进行调试/开发的那种数据。这确实意味着如果我想将实体链接在一起,我必须对已经插入的内容进行查询,否则我的代码将不知道它们生成的标识值,例如。在我为MyRoles
设置并完成context.SaveChanges
后,这种事情会出现在种子代码中:
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 脚本也不会与数据库无关。
所以我认为,如果您尝试设置实体的身份字段以进行 DB 播种数据,那么您肯定采取了错误的方法。
如果我实际上是将大量数据从 SQL Server 拖到 PostgreSQL(一个完整的实时数据库,而不仅仅是一些种子数据),我可以通过 EF 来完成,但我希望在同时,编写一些代码从源上下文中获取所有各种实体并将它们放入目标上下文中,然后保存更改。
一般来说,唯一适合插入标识值的情况是当您从一个 DB 复制到同一 DBMS 中的另一个 DB(SQL Server -> SQL Server、PostgreSQL -> PostgreSQL 等),然后您'会在 SQL 脚本中执行,而不是 EF 代码优先(SQL 脚本不会与 DB 无关,但它不需要;您不会在不同的 DBMS 之间切换)。
【讨论】:
有这样做的有效案例,例如从另一个系统导入数据。【参考方案7】:在尝试了在此站点上找到的几个选项后,以下代码对我有用 (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;
【讨论】:
【参考方案8】:我只是一名 DBA,但每当出现类似的情况时,我都会认为这是一种代码异味。也就是说,为什么你有任何依赖于具有特定标识值的某些行的东西?也就是说,在你上面的例子中,为什么 Novelette 夫人需要 106 的身份值?您可以获取她的身份值并在硬编码 106 的任何地方使用它,而不是一直依赖这种情况。有点麻烦,但更灵活(在我看来)。
【讨论】:
因为我正在从另一个数据库初始化数据。保留该数据库中的标识值并保持外键引用相同要简单得多。 身份值应始终被视为任意值。如果它们具有某些内在价值(正如您所认为的那样),请不要将该列设为身份列。 这适用于正常情况,但这是一些非常具体的数据的数据播种。我认为它不适用于这种特殊情况。实际上,如果您是从 SQL 脚本为数据库播种,您可能会在插入数据时打开 IDENTITY_INSERT,并指定身份字段值。【参考方案9】:有没有办法强制实体框架尝试插入实体的主键值?
是的,但没有我想看到的那么干净。
假设您使用的是自动生成的身份密钥,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】:我通过创建继承的上下文完成了这项工作:
我经常使用 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();
在任何事务之前检查迁移可能是一个好主意,使用“常规”上下文和配置:
【讨论】:
【参考方案11】:我找不到将记录插入表的方法。基本上,我创建了一个类似这样的 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。
【讨论】:
这非常糟糕,并且容易受到 SQL 注入的影响 - 请不要这样做,永远不要这样做! 我同意它很糟糕,但我只在未在实时代码中播种数据库时使用它,因此 SQL 注入不是问题。 这是唯一可行的方法。手动插入。应该防止 SQL 注入,但这是正确的方法,因为 EF 令人难以忍受。以上是关于如何强制实体框架插入标识列?的主要内容,如果未能解决你的问题,请参考以下文章
无法将值 NULL 插入列 - 如何强制标识增加? [关闭]
实体框架抛出无法在表中插入标识列的显式值...当 IDENTITY_INSERT 设置为 OFF 错误