假设我有一个非常简单的类:
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 中更新插入的其他问题,但对于这个问题仍然没有满意的答案。有没有一种方法可以在不违反唯一 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 命令
- 逐行尝试/捕捉
- 丑陋但有效
- 仅针对少数特殊实体(Web 应用程序)的锁定 + 全局上下文。
- 编码恐怖的方法但它有效
- BulkMerge 使用您自己的“主键”使用:http://entityframework-extensions.net/
- 付费但有效
免责声明:我是 Entity Framework Extensions 项目的所有者。
编辑 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 => 更新
- 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 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/