Entity Framework - Обработка нарушений PK / UKC 2601 для дублированных ключей - PullRequest
0 голосов
/ 09 января 2019

В какой-то момент в приложении происходит интенсивная обработка, и в 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 () при обработке исключений / дубликатов при переносе в транзакцию, если я распаковываю все из транзакции, она работает как надо.

1 Ответ

0 голосов
/ 09 января 2019

Я не вижу здесь 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);
                }
            }
        }
    }
}

Обратите внимание на гибкость и универсальность решения для проблемы параллелизма.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...