重访更新插入

Posted

技术标签:

【中文标题】重访更新插入【英文标题】:Upsert Revisited 【发布时间】:2016-05-05 03:28:23 【问题描述】:

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

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 StateStateName="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 StateStateName="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.

我已经检查了有关实体框架中的 upserts 的其他问题,但仍然没有对此问题满意的答案。假设并发更新插入来自不同机器上的客户端,有没有办法做到这一点而不会出现唯一的密钥违规?如何处理才能不产生异常?

【问题讨论】:

使用最后一种方法。但是您总是必须捕获唯一密钥违规。如果发生,请从数据库中获取现有状态。最后,只有数据库才能保证唯一性。 【参考方案1】:

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

有一些解决方案,但它们都只能在您调用 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 StateStateName="New York", Country="US"
            db.States.add(newState)
            db.SaveChanges();
        
    

为此案例创建自定义 SQL 命令 逐行尝试/捕获 丑陋但有效 锁定 + 全局上下文仅适用于少数特殊类型的实体(Web 应用程序)。 编码恐怖方法,但它有效 BulkMerge 使用您自己的“主键”使用:http://entityframework-extensions.net/ 付费但有效

免责声明:我是实体框架扩展项目的所有者。

编辑 1

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

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

你有 3 个解决方案(至少从我的帖子来看):

为此案例创建自定义 SQL(推荐) 逐行尝试/捕获 使用 BulkMerge

编辑 2:添加一些场景

举一个简单的例子,两个用户做同样的事情

using(var ctx = new EntitiesContext()) 

    State state = new StateStateName="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() // 糟糕!唯一密钥冲突错误

合并/批量合并

从 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 StateStateName="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 StateStateName = 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;

【讨论】:

关于您对 AddOrUpdate 的评论:由于我不知道主键(它是自动生成的),AddOrUpdate 甚至可以工作吗?您的锁定方法仅在只有一个客户端进程时才有效。对吗? @danb 使用 AddOrUpdate ,您可以指定要使用的谓词来代替主键 你是对的,很糟糕!好久没用这个方法了,你还是有同样的问题。 AddOrUpdate 在您调用该方法时做出添加或更新的决定,如果在此调用和 SaveChanges 调用之间添加了类似的值,则可能导致错误。 @ESG AddOrUpdate 仍然要求我必须知道主键,但这里不知道。 AddOrUpdate 如果被谓词使用,则不需要知道主键。如果实体存在,将从数据库中检索主键并执行更新,否则将添加实体。

以上是关于重访更新插入的主要内容,如果未能解决你的问题,请参考以下文章

为啥批量插入/更新更快?批量更新如何工作?

如何在 SQL Server 2005 中进行更新插入(更新或插入)

Sql Server 2005 - 插入更新触发器 - 获取更新,插入行

Slick 3.0 批量插入或更新(更新插入)

Spring MongoRepository 正在更新或更新插入而不是插入

如何在使用 jooq 生成的 dao 插入/更新后获取插入/更新的对象