首页 > 解决方案 > 通过实体框架更新时如何绕过唯一键约束(使用 dbcontext.SaveChanges())

问题描述

通过 EF 更新某些数据时遇到问题。

假设我的数据库中有一个表:

Table T (ID int, Rank int, Name varchar)

我有一个唯一的键约束Rank

例如,我在表中有以下数据:

链接到示例数据图像

我的 C# 对象是这样的:Person (name, rank),所以在前端,用户想要切换 Joe 和 Mark 的等级。

当我通过 EF 进行更新时,由于唯一键而出现错误。
我怀疑这是因为dbContext.SaveChanges使用了这种风格的更新:

UPDATE Table SET rank = 5 where Name = Joe
UPDATE Table SET rank = 1 where Name = Mark

使用 SQL 查询,我可以执行此更新:

将用户定义的表(排名、名称)从 C# 端传递到查询中,然后:

  update T 
  set T.Rank = Updated.Rank
  from Table T 
  inner join @UserDefinedTable Updated on T.Name = Temp.Name

这不会触发唯一键约束

但是我想使用 EF 进行此操作,我该怎么办?

到目前为止,我已经想到了这些其他解决方案:

注意:我上面使用的表结构和数据只是一个例子

有任何想法吗?

标签: c#.netsql-serverasp.net-mvcentity-framework

解决方案


您已经将大量精力放在 SQL 方面,但您可以在纯 EF 中做同样的事情。

下次为您提供 EF 代码将有所帮助,因此我们可以为您提供更具体的答案。

注意:不要在 EF 中使用此逻辑,因为 ReOrder 进程将所有记录加载到内存中,因此存在大量数据集,但它对于管理由附加过滤器子句限定的子列表或子列表中的序数很有用(因此不是整张桌子!)

如果您需要在整个表中执行唯一的排名逻辑,那么隔离的 ReOrder 进程本身就是一个很好的候选者,可以作为存储过程进入数据库

这里有两个主要变体(对于唯一值):

  1. 排名必须始终是连续的/连续的
    • 这简化了插入和替换逻辑,但您可能必须在代码中管理添加、插入、交换和删除方案。
    • 用于在等级中上下移动项目的代码非常容易实现
    • 必须管理删除以重新计算所有项目的排名
  2. 排名可能有差距(并非所有值都是连续的)
    • 这听起来应该更容易,但要评估在列表中上下移动意味着您必须考虑到差距。

      我不会发布此变体的代码,但请注意它通常维护起来更复杂。

    • 另一方面,您无需担心主动管理删除。

当需要管理序数时,我使用以下例程。

注意:此例程不会保存更改,它只是将所有可能受影响的记录加载到内存中,以便我们可以正确处理新的排名。

public static void ReOrderTableRecords(Context db)
{
    // By convention do not allow the DB to do the ordering. this type of query will load missing DB values into the current dbContext,  
    // but will not replace the objects that are already loaded.
    // The following query would be ordered by the original DB values:
    //      db.Table.OrderBy(x => x.Order).ToList()
    // Instead we want to order by the current modified values in the db Context. This is a very important distinction which is why I have left this comment in place.
    // So, load from the DB into memory and then order:
    //      db.Table[.Where(...optional filter by parentId...)].ToList().OrderBy(x => x.Order)
    // NOTE: in this implementation we must also ensure that we don't include the items that have been flagged for deletion. 
    var currentValues = db.Table.ToList()
                                .Where(x => db.Entry(x).State != EntityState.Deleted)
                                .OrderBy(x => x.Rank);
    int order = 1;
    foreach (var item in currentValues)
        item.Order = order++;
}

假设您可以将代码简化为将具有特定排名的新项目插入列表的函数,或者您想要交换列表中两个项目的排名:

public static Table InsertItem(Context db, Table item, int? Rank = 1)
{
    // Rank is optional, allows override of the item.Rank
    if (Rank.HasValue)
        item.Rank = Rank;

    // Default to first item in the list as 1
    if (item.Rank <= 0)
        item.Rank = 1;

    // re-order first, this will ensure no gaps.
    // NOTE: the new item is not yet added to the collection yet
    ReOrderTableRecords(db);

    var items = db.Table.ToList()
                        .Where(x => db.Entry(x).State != EntityState.Deleted)
                        .Where(x => x.Rank >= item.Rank);
    if (items.Any())
    {
        foreach (var i in items)
            i.Rank = i.Rank + 1;
    }
    else if (item.Rank > 1)
    {
        // special case
        // either ReOrderTableRecords(db) again... after adding the item to the table
        item.Rank = db.Table.ToList()
                            .Where(x => db.Entry(x).State != EntityState.Deleted)
                            .Max(x => x.Rank) + 1;
    }

    db.Table.Add(item);
    db.SaveChanges();
    return item;
}

/// <summary> call this when Rank value is changed on a single row </summary>
public static void UpdateRank(Context db, Table item)
{
    var rank = item.Rank;
    item.Rank = -1; // move this item out of the list so it doesn't affect the ranking on reOrder
    ReOrderTableRecords(db); // ensure no gaps

    // use insert logic
    var items = db.Table.ToList()
                        .Where(x => db.Entry(x).State != EntityState.Deleted)
                        .Where(x => x.Rank >= rank);
    if (items.Any())
    {
        foreach (var i in items)
            i.Rank = i.Rank + 1;
    } 
    item.Rank = rank;

    db.SaveChanges();
}

public static void SwapItemsByIds(Context db, int item1Id, int item2Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var item2 = db.Table.Single(x => x.Id == item2Id);

    var rank = item1.Rank;
    item1.Rank = item2.Rank;
    item2.Rank = rank;

    db.SaveChanges();
}

public static void MoveUpById(Context db, int item1Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var rank = item1.Rank - 1;
    if (rank > 0) // Rank 1 is the highest
    {
        var item2 = db.Table.Single(x => x.Rank == rank);
        item2.Rank = item1.Rank;
        item1.Rank = rank;
        db.SaveChanges();
    }
}
public static void MoveDownById(Context db, int item1Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var rank = item1.Rank + 1;
    var item2 = db.Table.SingleOrDefault(x => x.Rank == rank);
    if (item2 != null) // item 1 is already the lowest rank
    {
        item2.Rank = item1.Rank;
        item1.Rank = rank;
        db.SaveChanges();
    }
}

为确保不引入间隙,您应该在从表中删除项目之后调用,但在调用ReOrder 之前SaveChanges()

或者ReOrder在每个 Swap/MoveUp/MoveDown 之前调用类似于插入。


请记住,允许重复的 Rank 值要简单得多,尤其是对于大型数据列表,但您的业务需求将决定这是否是一个可行的解决方案。


推荐阅读