如何在 .net 核心中启用标识插入



我在EF中制作了一些表,并输入了一些种子数据,其中我用主键为几列赋予了值。当我运行应用程序时,我收到错误消息:

当 IDENTITY_INSERT 设置为 OFF 时,无法在表"人员"中插入标识列的显式值。

如何打开它?我在这里继续阅读以使用:

[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]

作为主键的属性上方。不幸的是,我仍然收到相同的错误消息。请帮忙。

我向具有主键的所有属性添加了[DatabaseGenerated(DatabaseGeneratedOption.None)]。当我运行迁移时,我可以看到身份列已被删除,但我仍然收到相同的错误消息。

当我进入SQL SEO时,我仍然可以在主键上看到身份列。我尝试刷新数据库。我做错了什么?我唯一能做的就是进入属性并删除标识,但为什么我不能按照上面提到的方式去做呢?

在 EF Core 1.1.2 中,我得到了它来处理事务。 在我的"数据库初始值设定项"中,将种子数据放入表中。 我使用了这个 EF6 答案中的技术。 下面是代码的示例:

using (var db = new AppDbContext())
using (var transaction = db.Database.BeginTransaction())
{
var user = new User {Id = 123, Name = "Joe"};
db.Users.Add(user);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT MyDB.Users ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT MyDB.Users OFF");
transaction.Commit();
}

必须处理相同的问题,这似乎是一个干净的解决方案。

信用>> https://github.com/dotnet/efcore/issues/11586

我做了一些更改,所以它现在适用于.Net Core 3.1 +(在.Net 5中测试),并且还添加了此方法SaveChangesWithIdentityInsert

public static class IdentityHelpers
{
public static Task EnableIdentityInsert<T>(this DbContext context) => SetIdentityInsert<T>(context, enable: true);
public static Task DisableIdentityInsert<T>(this DbContext context) => SetIdentityInsert<T>(context, enable: false);
private static Task SetIdentityInsert<T>(DbContext context, bool enable)
{
var entityType = context.Model.FindEntityType(typeof(T));
var value = enable ? "ON" : "OFF";
return context.Database.ExecuteSqlRawAsync(
$"SET IDENTITY_INSERT {entityType.GetSchema()}.{entityType.GetTableName()} {value}");
}
public static void SaveChangesWithIdentityInsert<T>(this DbContext context)
{
using var transaction = context.Database.BeginTransaction();
context.EnableIdentityInsert<T>();
context.SaveChanges();
context.DisableIdentityInsert<T>();
transaction.Commit();
}
}

用法

var data = new MyType{SomeProp= DateTime.Now, Id = 1};
context.MyType.Add(data);
context.SaveChangesWithIdentityInsert<MyType>();

基于忍者十字的答案改进的解决方案。

此代码直接添加到数据库上下文类中,并允许通过指定特定类型(映射到表)需要标识插入来保存更改。

目前,我只将其用于集成测试。

public async Task<int> SaveChangesWithIdentityInsertAsync<TEnt>(CancellationToken token = default)
{
await using var transaction = await Database.BeginTransactionAsync(token);
await SetIdentityInsertAsync<TEnt>(true, token);
int ret = await SaveChangesExAsync(token);
await SetIdentityInsertAsync<TEnt>(false, token);
await transaction.CommitAsync(token);
return ret;
}
private async Task SetIdentityInsertAsync<TEnt>(bool enable, CancellationToken token)
{
var entityType = Model.FindEntityType(typeof(TEnt));
var value = enable ? "ON" : "OFF";
string query = $"SET IDENTITY_INSERT {entityType.GetSchema()}.{entityType.GetTableName()} {value}";
await Database.ExecuteSqlRawAsync(query, token);
}

Steve Nyholm的答案工作正常,但我将提供一些额外的解释和一些带有异常处理的通用代码。

通常上下文负责事务,但在这种情况下,需要手动处理事务。为什么?

数据库上下文将在发出SET IDENTITY_INSERT后生成BEGIN TRAN。这将使事务的插入失败,因为IDENTITY_INSERT似乎会影响会话/事务级别的表。

因此,所有内容都必须包装在单个事务中才能正常工作。

以下是一些有用的代码,用于在密钥级别(而不是表级别)播种:

扩展.cs

[Pure]
public static bool Exists<T>(this DbSet<T> dbSet, params object[] keyValues) where T : class
{
return dbSet.Find(keyValues) != null;
}
public static void AddIfNotExists<T>(this DbSet<T> dbSet, T entity, params object[] keyValues) where T: class
{
if (!dbSet.Exists(keyValues))
dbSet.Add(entity);
}

数据库初始值设定项.cs

(假设模型类名与表名相同)

private static void ExecuteWithIdentityInsertRemoval<TModel>(AspCoreTestContext context, Action<AspCoreTestContext> act) where TModel: class
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT " + typeof(TModel).Name + " ON;");
context.SaveChanges();
act(context);
context.SaveChanges();
transaction.Commit();
}
catch(Exception)
{
transaction.Rollback();
throw;
}
finally
{
context.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT " + typeof(TModel).Name + " OFF;");
context.SaveChanges();
}
}
}
public static void Seed(AspCoreTestContext context)
{
ExecuteWithIdentityInsertRemoval<TestModel>(context, ctx =>
{
ctx.TestModel.AddIfNotExists(new TestModel { TestModelId = 1, ModelCode = "Test model #1" }, 1);
ctx.TestModel.AddIfNotExists(new TestModel { TestModelId = 2, ModelCode = "Test model #2" }, 2);
});
}

@sanm2009提出的解决方案包含一些不错的想法。

但是,该实现存在一些与错误使用Task/async/await相关的缺陷。

SaveChangesWithIdentityInsert 方法不返回 Task,也不等待对 EnableIdentityInsert 和 DisableIdentityInsert 的调用。

这可能会导致不良的副作用。

以下实现支持异步/等待和不可等待范例。

#region IDENTITY_INSERT
public static void EnableIdentityInsert<T>(this DbContext context) => SetIdentityInsert<T>(context, true);
public static void DisableIdentityInsert<T>(this DbContext context) => SetIdentityInsert<T>(context, false);
private static void SetIdentityInsert<T>([NotNull] DbContext context, bool enable)
{
if (context == null) throw new ArgumentNullException(nameof(context));
var entityType = context.Model.FindEntityType(typeof(T));
var value = enable ? "ON" : "OFF";
context.Database.ExecuteSqlRaw($"SET IDENTITY_INSERT {entityType.GetSchema()}.{entityType.GetTableName()} {value}");
}
public static void SaveChangesWithIdentityInsert<T>([NotNull] this DbContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
using var transaction = context.Database.BeginTransaction();
context.EnableIdentityInsert<T>();
context.SaveChanges();
context.DisableIdentityInsert<T>();
transaction.Commit();
}
#endregion 
#region IDENTITY_INSERT ASYNC
public static async Task EnableIdentityInsertAsync<T>(this DbContext context) => await SetIdentityInsertAsync<T>(context, true);
public static async Task DisableIdentityInsertAsync<T>(this DbContext context) => await SetIdentityInsertAsync<T>(context, false);
private static async Task SetIdentityInsertAsync<T>([NotNull] DbContext context, bool enable)
{
if (context == null) throw new ArgumentNullException(nameof(context));
var entityType = context.Model.FindEntityType(typeof(T));
var value = enable ? "ON" : "OFF";
await context.Database.ExecuteSqlRawAsync($"SET IDENTITY_INSERT {entityType.GetSchema()}.{entityType.GetTableName()} {value}");
}
public static async Task SaveChangesWithIdentityInsertAsync<T>([NotNull] this DbContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
await using var transaction = await context.Database.BeginTransactionAsync();
await context.EnableIdentityInsertAsync<T>();
await context.SaveChangesAsync();
await context.DisableIdentityInsertAsync<T>();
await transaction.CommitAsync();
}

#endregion 

另一种方法是显式打开连接,然后SET IDENTITY_INSERT <table> ON

var conn = context.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Posts ON");
var post = new WeblogPost()
{                    
Id= oldPost.Pk,  //   <!--- explicit value to Id field
Title = oldPost.Title,
...
};
context.Posts.Add(post);    
conn.Close();

显然,在 EF 请求之前显式打开连接后,EF 不会自动关闭该连接,因此该设置将应用于同一连接上下文。

这与史蒂夫对事务的响应起作用的原因相同,因为事务使连接保持活动状态。

注意:如果您计划稍后在应用程序/请求中再次使用相同的上下文,则不希望将连接放入using语句中。连接必须存在,因此清除连接上下文的最佳方法是.Close()它,从而将 EF 返回到其默认行为,即按操作打开和关闭连接。

@Steve Nyholm 的答案是可以的,但在 .Net core 3 中,ExecuteSqlCommand 已过时,ExecuteSql插值替换 ExecuteSqlCommand:

using (var db = new AppDbContext())
using (var transaction = db.Database.BeginTransaction())
{
var user = new User {Id = 123, Name = "Joe"};
db.Users.Add(user);
db.Database.ExecuteSqlInterpolated($"SET IDENTITY_INSERT MyDB.Users ON;");
db.SaveChanges();
db.Database.ExecuteSqlInterpolated($"SET IDENTITY_INSERT MyDB.Users OFF");
transaction.Commit();
}

另一种方法是使用 ExecuteSqlRaw。与 ExecuteSql插值不同,您不必将传递的字符串转换为格式化字符串类型。

using (var db = new AppDbContext())
using (var transaction = db.Database.BeginTransaction())
{
var user = new User {Id = 123, Name = "Joe"};
db.Users.Add(user);
db.Database.ExecuteSqlRaw("SET IDENTITY_INSERT MyDB.Users ON");
db.SaveChanges();
db.Database.ExecuteSqlRaw("SET IDENTITY_INSERT MyDB.Users OFF");
transaction.Commit();
}

以下解决方案对我有用。链接) 我在下面添加了注释。并删除了[Key]注释。

[KeyAttribute()]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }

命名空间可以根据实体框架版本进行更改。对于实体框架,核心命名空间System.ComponentModel.DataAnnotations.Schema自从我在一个新项目中尝试过以来,我没有面临数据迁移。

为了使用DbContext添加具有对象图的相关实体,我使用了一个DbCommandInterceptor,它会自动为有问题的表设置INSERT_IDENTITY ON,然后在插入后OFF。这适用于手动设置和DbContext.SaveChanges的 ID。我在集成测试中使用它,但在性能优化之后,在某些情况下它可能适用于生产代码。这是我对一个类似的SO问题的回答,它解释了细节。

如果不想使用 EF core 的自动生成主键值功能,可以将其关闭。您可以将数据添加到主键 它应该解决错误 - 设置标识插入关闭

[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int StudentId { get; set; }

将"数据库生成"选项设置为"无"对我有帮助。您可以在此处找到有关它的更多信息- https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations

在事务中使用"SET IDENTITY_INSERT [table] ON/OFF">

public static void TranslateDatabase(ref BDVContext bdvContext) 
{
bdvContext.Foro.RemoveRange(bdvContext.Foro);
bdvContext.SaveChanges();
using (var transaction = bdvContext.Database.BeginTransaction()) 
{ 
bdvContext.Database.ExecuteSqlRaw("SET IDENTITY_INSERT [dbo].[Foro] On");
using (old_balsaContext db = new old_balsaContext()) 
{
long id = 0;
foreach (ForoA77 post in db.ForoA77.Where(x => x.Fecha > new DateTime(2000,1,1) & x.IdPadre == 0 ) )
{
bdvContext.Foro.Add(new Foro 
{
Id = ++id
, ParentId = 0
, EditId = 0
, IdDomains = 2
, UserNick = post.IdUsuario == 1 ? bdvContext.Users.Where(x => x.Id == 2).Single().User : post.Nick?? ""
, IdUsers = post.IdUsuario == 1 ? (int?)2 : null
, Title = post.Asunto?? ""
, Text = post.Texto?? ""
, Closed = post.Cerrado?? false
, Banned = post.Veto?? false
, Remarqued = post.Remarcado?? false
, Deleted = false
, Date = post.Fecha?? new DateTime(2001,1,1)
});
}
}
bdvContext.SaveChanges();
bdvContext.Database.ExecuteSqlRaw("SET IDENTITY_INSERT [dbo].[Foro] Off");
transaction.Commit();
}
}

注意,我的实体框架是通过逆向工程生成的

最新更新