数据库 – 重新访问Upsert

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

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

其中StateId是自动生成的主键. StateName和County是一个唯一的密钥.假设您要为给定的StateName和Country插入新的State(如果它不在db中).其他类似问题中提出的各种解决方案在这里不起作用:

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.

我已经检查了有关实体框架中的upserts的其他问题,但对这个问题仍然没有一个满意的答案.有没有一种方法可以做到这一点,而不会得到一个独特的密钥违规,因为来自不同机器上的客户端会发生并发的upserts?如何处理以免产生异常?

最佳答案 “AddOrUpdate”和你的第二个场景在你调用方法/查询时检查行是否已经存在,而不是在“SaveChanges”期间,这是你注意到的concurrents 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 State{StateName="New York", Country="US"}
            db.States.add(newState)
            db.SaveChanges();
        }
    }
}

>为此案例创建自定义SQL命令
>逐行尝试/捕获

>丑陋但它有效

>仅针对少数特殊类型的实体(Web应用程序)锁定全局上下文.

>编码恐怖方法,但它的工作

> BulkMerge使用您自己的“主键”使用:http://entityframework-extensions.net/

>付费但有效

免责声明:我是Entity Framework Extensions项目的所有者.

编辑1

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

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

你剩下3个解决方案(至少从我的帖子中):

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

编辑2:添加一些场景

让我们举一个简单的例子,其中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()//找到数据,SET PK为10 => UPDATE
> UserB:SaveChanges()//更新数据

案例2:

这种情况失败,您需要捕获错误并做一些事情

> UserA:AddOrUpdate()//找不到任何内容=>加
> UserB:AddOrUpdate()//找不到任何内容=>加
> UserA:SaveChanges()//使用PK = 10添加
> UserB:SaveChanges()//哎呀!唯一键违规错误

Merge / BulkMerge

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

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

通过BulkMerge(执行SQL Merge)的示例,并发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;
}
点赞