实体框架-处理PK/UKC 2601违反重复密钥的行为



在应用程序中的某个时刻,需要进行一些繁重的处理,并且会在dbcontext中创建一个包含多个不同实体的可观的图以供插入。考虑以下实体,作为更大模型的一部分:

public class Wall
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
public ICollection<User> Users { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Labels> Labels { get; set; }
}
public class Label
{
public int Id { get; set; }
[Index("IX_UniqueNameKind", IsUnique = true, Order = 1)]
[MaxLength(255)]
public string Name { get; set; }
[Index("IX_UniqueNameKind", IsUnique = true, Order = 2)]
[MaxLength(60)]
public string Kind { get; set; }
public ICollection<Post> Posts { get; set; }
}

我在Post和Label之间有一个多对多的关系,关联表"PostLabel"可以避免冗余的数据库条目并优化空间使用。每个标签的唯一性由"名称"one_answers"种类"定义。

当多个用户可能正在运行相同的进程并插入相同的标签(名称、种类),导致EntityFramework的SaveChanges引发DbUpdateException异常时,就会出现此问题。

目前,我正在分离未能插入的"标签",并将现有的"标签"与数据库关联。

public override int SaveChanges()
{   
while (!isSaved)
{
try
{
// save data
result = base.SaveChanges();
// set flag to exit loop
isSaved = true;
}
catch (DbUpdateException ex)
{
var sqlException = ex.InnerException?.InnerException as SqlException;
if (sqlException != null && sqlException.Errors.OfType<SqlError>().Any(se => se.Number == 2601 || se.Number == 2627) && ex.Entries.All(e => e.Entity.GetType() == typeof(Label))
{
// handle duplicates: find existing record in DB and associate it to the parent Post entity.
var entries = ex.Entries;
foreach (var entry in entries)
{
HandleLabelDuplicates(entry);
}
}
else
{
throw;
}
}
}
return result;
}
private void HandleSourceSegmentLabelDuplicates(DbEntityEntry entry)
{
var labelWhichFailedToInsert = (Label)entry.Entity;
var labelAlreadyInDatabase = Labels.Single(t => t.Name.Equals(labelWhichFailedToInsert.Name) && t.Kind.Equals(labelWhichFailedToInsert.Kind));
// fix label association in all "Posts" which contain this label.
foreach (var post in labelWhichFailedToInsert.Posts)
{
// fix the reference to the existing label in the database, instead of inserting a new one.
post.Labels.Add(labelAlreadyInDatabase);
}
// change state to remove it from context
entry.State = EntityState.Detached;
}

这里的问题是,整个DbContext被插入多次,更准确地说,每次处理异常加1,所以如果发现一个重复,整个模型就会在DB中插入两次。

我的猜测是,在第一次尝试SaveChanges时,所有成功插入的实体都不会将其状态更新为"Unchanged",因为引发了异常,但插入是在SQL事务中,因此,第二次尝试SaveChange时将再次插入它们。

有什么想法吗?

编辑:整个工作都是在一个事务中完成的:

using (var transaction = context.Database.BeginTransaction())
{
// some work
context.Orders.Add(order);
context.SaveChanges();
// some more work where some id's are needed
context.SaveChanges();
transaction.Commit();
return order.Id;
}

问题似乎是在处理事务中包装的异常/重复时重复SaveChanges(),如果我从事务中打开所有内容,它就会正常工作。

我在这里没有看到lock(),这可能是您可以考虑的解决方案之一。锁定操作,等待更新完成,然后再次继续。

其次,我没有看到DbUpdateConcurrencyException处理,所以您可以考虑的另一个解决方案是:

using (var context = new Ctx())
{
//your logic
while (!saved)
{
try
{
// Attempt to save changes to the database
context.SaveChanges();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is YourModel)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
// TODO: decide which value should be written to database
// proposedValues[property] = <value to be saved>;
}
// Refresh original values to bypass next concurrency check
entry.OriginalValues.SetValues(databaseValues);
}
else
{
throw new NotSupportedException(
"Don't know how to handle concurrency conflicts for "
+ entry.Metadata.Name);
}
}
}
}
}

注意并发问题解决方案的灵活性和多功能性。

最新更新