更新两个实体时,实体框架不一致生成正确的 UPDATE 语句

问题描述 投票:0回答:1

背景

我最近继承了这个ASP.NET 4.8项目。我正在实现将数据库中的一行设置为默认记录的功能。这是由 SQL Server 支持的,当设置

UNIQUE
位列时,该 SQL Server 在
CustomerID
列上有一个
IsDefault
索引。

问题

当请求新的默认记录时,我必须在更新新记录之前将现有默认值设置为

False
,否则将违反
UNIQUE
数据库约束。有时,会发生正确的更新,但我无法确定发生这种情况的状态。有时是在应用程序首次启动时,或者在应用程序外部编辑数据库记录时。

大多数后续请求永远不会更新现有的默认记录。检查生成的 SQL 语句表明,当前默认记录的 SQL

UPDATE
语句实际上从未由实体框架生成。

这是控制器代码,它包含在相同的

using
语句中,因此每个查询都使用相同的
dbContext
:

CustomerPO existingPO = (from item in dbContext.CustomerPO 
                         where (item.PurchaseOrderID == purchaseOrder.PurchaseOrderID) 
                         select item).FirstOrDefault();

var currentDefault = (from item in dbContext.CustomerPO
                      where item.IsDefault && item.CustomerID == existingPO.CustomerID
                      select item).FirstOrDefault();

if (currentDefault != null && currentDefault.PurchaseOrderID != existingPO.PurchaseOrderID)
{
    currentDefault.IsDefault = false;
}

dbContext.Entry(existingPO).CurrentValues.SetValues(purchaseOrder);
dbContext.SaveChanges();

return Ok(new { PurchaseOrderID = purchaseOrder.PurchaseOrderID });

尝试过什么

  1. 指定整个实体被修改。这会导致同样的问题。
dbContext.Entry(currentDefault).State = System.Data.Entity.EntityState.Modified;
  1. 指定特定列已被修改。这会导致同样的问题。
dbContext.Entry(currentDefault).Property(e => e.IsDefault).IsModified = true;
  1. 迭代
    dbContext.ChangeTracker.Entries()
    显示
    currentDefault
    实体被标记为
    Modified
    并且
    IsDefault
    属性被标记为相同。

我已经阅读了一些描述上述内容的其他在线帖子。我试图完成的事情似乎是可能的,我只是没有得到我期望的输出。

问题

为了澄清,我可以通过对

SaveChanges
进行两次
dbContext
调用来保存记录。事实上,我的上述代码偶尔可以工作,这让我觉得我缺少关于实体框架的一个警告。

  1. 我遇到与
    UNIQUE
    索引相关的限制吗?
  2. 是否需要设置其他数据库上下文设置来执行此类操作?
  3. 按照这段代码的编写方式,EF 不支持多个
    UPDATE
    调用吗?
  4. 这种情况需要交易吗?

我很感激对此的任何见解 - 文档、链接、个人经验等。谢谢。

asp.net-web-api entity-framework-6
1个回答
0
投票

尝试根据唯一约束更新两条记录时,很多情况可能会出错,其中一个必须放弃一个值(例如“IsDefault”),然后另一个才能保留该值。不仅存在其他系统/会话并发更新的问题,而且如果没有直接关系,我们就无法保证更新将以可预测的顺序发生。

选项1:处理异常。由于肯定存在超出我们控制范围的情况,可能会导致错误,因此一种选择是处理错误并重试或提醒用户注意刷新并重试操作。

选项 2:单独显式提交更改以确保它们按顺序完成。

CustomerPO existingPO = (from item in dbContext.CustomerPO 
    where (item.PurchaseOrderID == purchaseOrder.PurchaseOrderID) 
    select item).FirstOrDefault();

var currentDefault = (from item in dbContext.CustomerPO
    where item.IsDefault && item.CustomerID == existingPO.CustomerID
    select item).FirstOrDefault();

using var transaction = dbContext.Database.BeginTransaction();

if (currentDefault != null && currentDefault.PurchaseOrderID != existingPO.PurchaseOrderID)
{
    currentDefault.IsDefault = false;
    dbContext.SaveChanges();
}

dbContext.Entry(existingPO).CurrentValues.SetValues(purchaseOrder);
dbContext.SaveChanges();
transaction.Commit();

考虑到两次保存,这并不理想,我们需要一个事务来确保这两个更改一起提交或回滚。否则,我们可能会遇到这样的情况:我们删除 IsDefault 状态而没有成功设置新记录。

选项 3:使用触发器管理 IsDefault。我最近需要对保存的搜索执行类似的操作,其中记录了最后选择的搜索。当用户选择保存的搜索条件时,我会更新它以将其标记为已选择,以便下次返回时它将成为默认值。

ALTER TRIGGER [dto].[CustomersSingleIsDefault] 
   ON  [dro].[Customers] 
   AFTER INSERT, UPDATE
AS 
BEGIN

    SET NOCOUNT ON;

    DECLARE @isSelected AS BIT,
        @customerId AS INT,
        @typeName AS VARCHAR(500);

    -- Don't allow this trigger to trip recursively as we will be updating rows.
    IF TRIGGER_NESTLEVEL(@@PROCID) > 1
        RETURN;

    IF NOT UPDATE(IsDefault)
        RETURN;

    SELECT @customerId = CustomerId, @isDefault = IsDefault 
    FROM INSERTED;

    IF @isDefault = 1
    BEGIN
        UPDATE [dto].Customers SET IsDefault = 0 
        WHERE CustomerId = @customerId AND IsDefault = 1; 
    END;

END

触发器的优点是它会针对任何更改表的进程运行,以确保选择一条记录。对于 EF Core 实体,您需要标记客户实体以告诉 EF 它有关联的触发器。通常与:

builder.ToTable(tb => tb.UseSqlOutputClause(false));

需要注意的是向表中添加触发器及其潜在的性能影响。

考虑到您有与此 IsDefault 标志相关的唯一约束,需要检查的另一个细节是可能会出现导致尝试创建多个非默认(IsDefault = false)行的情况。如果您认为应该找到非默认行并将其标记为默认的代码未按预期工作,而是插入新的“IsDefault=true”行,则可能会发生这种情况。我最近不得不通过软删除来追踪与此类似的系统中的错误,其中 IsActive 标志成为唯一约束的一部分。系统大部分时间都在工作,直到您重新激活以前不活动的行。一个错误发现它找不到要重新激活的非活动行并插入了一个新的活动行。 (没有错误)但是稍后当用户去激活该新项目时,由于现在有两个不活动的行,约束失败。这是一个令人烦恼的错误,因为它仅在最初的错误造成无效数据场景后很长时间才显现出来。 (插入而不是重新激活)

© www.soinside.com 2019 - 2024. All rights reserved.