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

时间:2019-01-09 13:37:31

标签: c# sql entity-framework

在应用程序中的某个时刻,需要进行大量的处理,并且在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”,以避免冗余的数据库条目并优化空间使用。每个标签的唯一性都由“名称”和“种类”定义。

当多个用户可能正在运行相同的进程并插入相同的标签(名称,种类),从而导致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时,因此,如果发现一个重复项,则整个模型将在数据库中插入两次。

我的猜测是,在第一次尝试SaveChanges时,成功插入的所有实体都不会因为抛出异常而将其状态更新为“未更改”,但是插入是在SQL事务中进行的,因此是第二次尝试保存到SaveChanges将再次插入它们。

有什么想法吗?

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

        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 :(得分:0)

我在这里没有看到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);
                }
            }
        }
    }
}

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