通过实体框架更新时如何绕过唯一键约束(使用 dbcontext.SaveChanges())

Posted

技术标签:

【中文标题】通过实体框架更新时如何绕过唯一键约束(使用 dbcontext.SaveChanges())【英文标题】:How to get around unique key constraint when updating via Entity Framework (using dbcontext.SaveChanges()) 【发布时间】:2019-11-11 01:49:38 【问题描述】:

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

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

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

我对@9​​87654324@ 有一个唯一的键约束。

例如,我在表中有这样的数据:

我的 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 进行此操作,我该怎么办?

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

删除旧记录,通过 EF 从更新的对象中添加“新”记录

删除数据库上的唯一约束并编写一个 C# 函数来完成唯一约束的工作

只需使用上面示例中的 SQL 查询,而不是 EF

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

有什么想法吗?

【问题讨论】:

【参考方案1】:

想法 - 您可以将其作为两步操作(包装为单个事务)

1) 为所有必须更新为负数的实体设置值(Joe, -1; Mark -5)

2) 设置为正确的值(Joe,5,Mark 1)


SQL Server 的等价物:

SELECT 1 AS ID, 1 AS [rank], 'Joe' AS name INTO t
UNION SELECT 2,2,'Ann'
UNION SELECT 3,5,'Mark'
UNION SELECT 4,7,'Sam';

CREATE UNIQUE INDEX uq ON t([rank]);

SELECT * FROM t;

/* Approach 1
UPDATE t SET [rank] = 5 where Name = 'Joe';
UPDATE t SET [rank] = 1 where Name = 'Mark';

Cannot insert duplicate key row in object 'dbo.t' with unique index 'uq'.
The duplicate key value is (5). Msg 2601 Level 14 State 1 Line 2
Cannot insert duplicate key row in object 'dbo.t' with unique index 'uq'.
The duplicate key value is (1).
*/

BEGIN TRAN
-- step 1

UPDATE t SET [rank] = -[rank] where Name = 'Joe';
UPDATE t SET [rank] = -[rank] where Name = 'Mark';


-- step 2
UPDATE t SET [rank] = 5 where Name = 'Joe'
UPDATE t SET [rank] = 1 where Name = 'Mark';
COMMIT;

db<>fiddle demo

【讨论】:

这实际上是一个非常绝妙的主意——尽管不幸的是,我认为在我的情况下这是不可能的。目前的设计是,即使不需要更新,它也会循环遍历每条记录(是的,设计很糟糕,但在我的情况下,在最坏的情况下,它只会循环遍历 12-15 条记录)。所以理论上我会为每一行设置它为负数【参考方案2】:

您在 SQL 方面做了很多工作,但您可以在纯 EF 中做同样的事情。

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

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

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

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

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

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

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

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

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

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()之前调用

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


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

【讨论】:

以上是关于通过实体框架更新时如何绕过唯一键约束(使用 dbcontext.SaveChanges())的主要内容,如果未能解决你的问题,请参考以下文章

无法在实体框架中更新具有唯一约束索引的实体

尝试持久化和具有主键唯一约束的实体时出错

android开发三大框架

实体框架:0..1 对多外键约束无法识别?

使用Save_Changes实体框架外键违规

更新存储过程实体框架抛出“验证FunctionImport名称是唯一的”错误