我想编写一些 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()
时,我被告知“当 IDENTITY_INSERT 设置为 ON 时,必须为表 'MyUser' 中的标识列指定显式值或者当复制用户插入 NOT FOR REPLICATION 标识列时。”
据推测,因为 MyUserId(即 MyUser 表中的 Identity 列)是主键,所以实体框架在我调用 context.SaveChanges()
时不会尝试设置它,即使我为 MyUser
实体提供了 { 3}财产。
那么有没有办法强制实体框架尝试插入实体的主键值呢?或者可能是一种临时将 MyUserId
标记为不是主键值的方法,以便 EF 尝试插入它?
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' 中插入标识列的显式值”。
您不需要对连接做任何有趣的事情,您可以去掉中间人,只需使用 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 指示该信息?
聚会有点晚了,但如果有人首先在 EF5 中使用 DB 遇到这个问题:我无法让任何一种解决方案都能正常工作,但找到了另一种解决方法:
在运行 .SaveChanges()
命令之前,我重置了表的身份计数器:
Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1););
Entities.YourTable.Add(newObject);
Entities.SaveChanges();
这意味着每次添加后都需要应用 .SaveChanges()
- 但至少它有效!
-1
。
这是问题的解决方案。我已经在 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 生成的选项,例如,
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();
}
这个想法只有在目标表为空,或者插入的记录的 id 高于表中所有现有 id 的情况下才有效!
3 年过去了,我在将生产数据传输到测试系统时遇到了类似的问题。用户希望能够随时将生产数据复制到测试系统中,因此我没有在 SQL Server 中设置传输作业,而是寻找一种使用现有 EF 类在应用程序中完成传输的方法。通过这种方式,我可以为用户提供一个菜单项,以便在他们想要的时候开始传输。
该应用程序使用 MS SQL Server 2008 数据库和 EF 6。由于这两个数据库通常具有相同的结构,我认为我可以通过使用 AsNoTracking()
读取每个实体的记录来轻松地将数据从一个 DbContext 实例传输到另一个实例,而只需 {2 }(或 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});"
经过仔细考虑,我认为实体框架拒绝插入标识列是一个特性,而不是一个错误。 :-) 如果我要在我的数据库中插入所有条目,包括它们的标识值,我还必须为实体框架为我自动创建的每个链接表创建一个实体!这不是正确的方法。
所以我要做的是设置只使用 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);
这样做也使我更有可能在更改架构时更新我的播种数据,因为我可能会在更改它时破坏播种代码(如果我删除字段或实体,则使用该字段的现有播种代码/entity 将无法编译)。使用 SQL 脚本进行播种,情况并非如此,SQL 脚本也不会与数据库无关。
所以我认为,如果您尝试设置实体的身份字段以进行 DB 播种数据,那么您肯定采取了错误的方法。
如果我实际上将大量数据从 SQL Server 拖到 PostgreSQL(一个完整的实时数据库,而不仅仅是一些种子数据),我可以通过 EF 来完成,但我希望同时打开两个上下文,并编写一些代码以从源上下文中获取所有各种实体并将它们放入目标上下文中,然后保存更改。
通常,唯一适合插入标识值的时间是当您从一个 DB 复制到同一 DBMS 中的另一个 DB(SQL Server -> SQL Server、PostgreSQL -> PostgreSQL 等)时,然后您会这样做它在 SQL 脚本中,而不是 EF 代码优先(SQL 脚本不会与 DB 无关,但它不需要;您不会在不同的 DBMS 之间切换)。
在尝试了在此站点上找到的几个选项后,以下代码对我有用(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;
}
我通过创建一个继承的上下文来完成这项工作:
我与 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();
}
}
}
}
在任何事务之前检查迁移可能是一个好主意,使用“常规”上下文和配置:
我只是一名 DBA,但每当出现这样的事情时,我都认为这是一种代码味道。也就是说,为什么你有任何依赖于具有特定标识值的某些行的东西?也就是说,在您上面的示例中,为什么 Novelette 夫人需要 106 的身份值?与其一直依赖这种情况,您可以获取她的身份值并在硬编码 106 的任何地方使用它。有点麻烦,但更灵活(在我看来)。
有没有办法强制实体框架尝试插入实体的主键值?
是的,但没有我想看到的那么干净。
假设您使用的是自动生成的身份密钥,EF 将完全忽略您存储密钥值的尝试。由于上面详述的许多充分理由,这似乎是“设计使然”,但有时您仍想完全控制种子数据(或初始负载)。我建议 EF 在未来的版本中加入这种播种。但在他们这样做之前,只需编写一些在框架内工作的代码并自动处理混乱的细节。
Eventho VendorID 被 EF 忽略,您可以将其与基本循环和计数一起使用,以确定在您的实时记录之间添加多少占位符记录。占位符在添加时被分配下一个可用的 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 有序。
这成功了(我现在碰巧使用的是 EFCore 3.x,肯定对以后的版本有效)。在您第一次调用 SaveAsync
之前的某个时间,使用 true
的输入调用它(即将 IDENTITY_INSERT
设置为 ON
)。完成后用 false
再次调用它(可能不需要)。关键是必须设置 OpenConnection
才能使这项工作。如果在一个上下文中多次保存,根据我的经验,一开始仍然只需要调用一次,但即使多次调用/设置为 true 也没有什么坏处。
public void Set_IDENTITY_INSERT(bool turnOn, DbContext context, string tableName)
{
var db = context.Database;
string command = $"SET IDENTITY_INSERT {tableName} {(turnOn ? "ON" : "OFF")}";
if(turnOn)
db.OpenConnection();
db.ExecuteSqlCommand(command);
if(!turnOn)
db.CloseConnection();
}
我找不到将记录插入表的方法。基本上,我用这样的东西创建了一个 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。