В какой-то момент в приложении происходит интенсивная обработка, и в 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», чтобы избежать избыточных записей в БД и оптимизировать использование пространства. Уникальность каждого ярлыка определяется «name» и «kind».
Проблема возникает, когда несколько пользователей могут запускать один и тот же процесс и вставлять одну и ту же метку (имя, вид), в результате чего SaveChanges EntityFramework выдает исключение 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, поэтому, если обнаружен один дубликат, вся модель вставляется дважды в БД.
Я предполагаю, что при первой попытке SaveChanges все успешно вставленные сущности не обновляют свое состояние до "Неизменено", так как генерируется исключение, однако вставки выполняются в транзакции SQL, следовательно, вторая попытка SaveChanges вставит их снова.
Есть идеи?
EDIT:
Вся работа выполняется в рамках транзакции:
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 () при обработке исключений / дубликатов при переносе в транзакцию, если я распаковываю все из транзакции, она работает как надо.