假设我有一个非常简单的类:

public class State
{
    public int StateId { get; set; }
    public string StateName { get; set; }
    public string Country { get; set; }
}

其中 StateId 是自动生成的主键。 StateName 和 County 是唯一键。假设您想为给定的 StateName 和 Country 插入一个新的 State(如果它不在数据库中)。其他类似问题中提出的各种解决方案在这里不起作用:
State state = new State{StateName="New York", Country="US"};

db.States.add(state);  //Does not work because we can get a unique key violation.

db.States.addOrUpdate(state); does not work because the primary key is not known.

最后是最有前途的一个:
var stateQuery = from state in db.States
                 where StateName = "New York"
                 and Country = "US"
                 select state;
State newState = stateQuery.FirstOrDefault();
if(newState != null) return newState;
newState = new State{StateName="New York", Country="US"}
db.States.add(newState)
return newState;
//does not work because it can generate a unique key violoation if there are
//concurrent upserts.

我已经检查了有关 Entity Framework 中 upsert 的其他问题,但仍然没有对这个问题满意的答案。假设并发 upsert 来自不同机器上的客户端,有没有办法做到这一点而不会导致唯一的 key 冲突?这如何处理才能不产生异常?

最佳答案

“AddOrUpdate”和您的第二个场景在您调用方法/查询时检查该行是否已经存在,而不是在“SaveChanges”期间检查该行是否已经存在,这是您注意到的并发更新插入的问题。

有一些解决方案,但它们都只能在您调用 SaveChanges 时完成:

使用锁(Web 应用程序)

使用锁和您的第二个场景来确保 2 个用户不能同时尝试添加状态。如果它适合您的场景,则推荐。

lock(stateLock)
{
    using(var db = new MyContext)
    {
        var state = (from state in db.States
                         where StateName = "New York"
                         and Country = "US"
                         select state).FirstOrDefault();

        if(state == null)
        {
            State newState = new State{StateName="New York", Country="US"}
            db.States.add(newState)
            db.SaveChanges();
        }
    }
}
  • 为这种情况创建自定义 SQL 命令
  • 逐行尝试/捕获
  • 丑,但它有效
  • Lock + Global Context 仅用于少数特殊类型的实体(Web 应用程序)。
  • 编码恐怖方法但它有效
  • BulkMerge 使用您自己的“主键”使用:http://entityframework-extensions.net/
  • 已付费但它有效

  • 免责声明 :我是 Entity Framework 扩展项目的所有者。

    编辑 1

    AddOrUpdate 方法永远不会工作,因为它在调用方法时决定“添加”或“更新”。但是,并发用户仍然可以在 AddOrUpdate 和 SaveChanges 调用之间插入类似的值(唯一键)。

    锁定方法仅适用于 Web 应用程序,所以我想它不适合您的场景。

    你剩下 3 个解决方案(至少从我的帖子中):
  • 为这种情况创建自定义 SQL(推荐)
  • 逐行尝试/捕获
  • 使用批量合并

  • 编辑 2 :添加一些场景

    让我们举一个简单的例子,两个用户做同样的事情
    using(var ctx = new EntitiesContext())
    {
        State state = new State{StateName="New York", Country="US"};
    
        // SELECT TOP (2) * FROM States WHERE (N'New York' = StateName) AND (N'US' = Country)
        ctx.States.AddOrUpdate(x => new {x.StateName, x.Country }, state);
    
        // INSERT: INSERT INTO States VALUES (...); SELECT ID
        // UPDATE: Perform an update on different column value retrieved from AddOrUpdate
        ctx.SaveChanges();
    }
    

    情况1:

    这种情况下工作正常,因为没有发生并发保存
  • UserA: AddOrUpdate()//没有找到 => 添加
  • UserA: SaveChanges()//添加 PK = 10
  • UserB: AddOrUpdate()//找到数据,将 PK 设置为 10 => UPDATE
  • UserB: SaveChanges()//更新数据

  • 案例2:

    这种情况失败,您需要捕获错误并做一些事情
  • UserA: AddOrUpdate()//没有找到 => 添加
  • UserB: AddOrUpdate()//没有找到 => 添加
  • UserA: SaveChanges()//添加 PK = 10
  • UserB: SaveChanges()//糟糕!唯一 key 冲突错误

  • 合并/批量合并

    创建从 SQL 合并以支持并发 UPSERT:

    https://msdn.microsoft.com/en-CA/library/bb510625.aspx

    以 BulkMerge(执行 SQL 合并)为例,并发 UPSERT 不会导致任何错误。
    using(var ctx = new EntitiesContext())
    {
        List<State> states = new List<State>();
        states.Add(new State{StateName="New York", Country="US"});
        // ... add thousands of states and more! ...
    
        ctx.BulkMerge(states, operation =>
            operation.ColumnPrimaryKeyExpression = x => new {x.StateName, x.Country});
    }
    

    编辑 3

    你是对的,合并需要一些隔离来减少更多或根本没有冲突的机会。

    这是单一实体方法。使用双“AddOrUpdate”,几乎不可能代码无法并发添加,但是,此代码不是通用的,并且需要进行 3-4 次数据库往返,因此不建议在任何地方使用,而仅适用于少数实体。
    using (var ctx = new TestContext())
    {
        // ... code ...
    
        var state = AddOrUpdateState(ctx, "New York", "US");
    
        // ... code ...
    
        // Save other entities
        ctx.SaveChanges();
    }
    
    public State AddOrUpdateState(TestContext context, string stateName, string countryName)
    {
        State state = new State{StateName = stateName, Country = countryName};
    
        using (var ctx = new TestContext())
        {
            // WORK 99,9% of times
            ctx.States.AddOrUpdate(x => new {x.StateName, x.Country }, state);
    
            try
            {
                ctx.SaveChanges();
            }
            catch (Exception ex)
            {
                // WORK for the 0.1% time left
                // Call AddOrUpdate to get properties modified
                ctx.States.AddOrUpdate(x => new {x.StateName, x.Country }, state);
                ctx.SaveChanges();
    
                // There is still have a chance of concurrent access if
                // A thread delete this state then a thread add it before this one,
                // But you probably have better chance to have GUID collision then this...
            }
        }
    
        // Attach entity to current context if necessary
        context.States.Attach(state);
        context.Entry(state).State = EntityState.Unchanged;
    
        return state;
    }
    

    关于database - 重新插入更新,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/35049901/

    10-09 22:15