C# 当行可能不存在时,如何增加原子增量?
假设我有一个C# 当行可能不存在时,如何增加原子增量?,c#,.net,entity-framework,entity-framework-6,C#,.net,Entity Framework,Entity Framework 6,假设我有一个DbSet Items的MyContext派生自DbContext,其中Item定义如下 public class Item { [Key] public string Key { get; set; } [ConcurrencyCheck] public int Value { get; set; } } 给定一个键,我希望以原子方式递增相应的值,其中表中尚未包含的键的隐式值为0。也就是说,对表中不存在的键进行原子递增将得到值1。以下是我目前的做
DbSet Items
的MyContext
派生自DbContext
,其中Item
定义如下
public class Item
{
[Key]
public string Key { get; set; }
[ConcurrencyCheck]
public int Value { get; set; }
}
给定一个键,我希望以原子方式递增相应的值,其中表中尚未包含的键的隐式值为0。也就是说,对表中不存在的键进行原子递增将得到值1。以下是我目前的做法:
public static async Task IncrementAsync(string key)
{
using (var context = new MyContext())
{
while (true)
{
var item = await context.Items.FindAsync(key);
if (item == null)
{
context.Items.Add(new Item { Key = key, Value = 1 });
}
else
{
item.Value++;
}
try
{
await context.SaveChangesAsync();
break;
}
catch (DbUpdateException)
{
continue;
}
}
}
}
当对IncrementAsync
的多个调用同时运行时,如果处于活动锁定状态,则此操作将失败
- 正确的方法是什么
循环是否应该在使用while
的
之外,以便每次尝试都获得一个新的上下文?我试过这个,它让一切都正常,但我觉得我在创建和破坏这么多上下文方面效率低下
- 我是否遗漏了上下文跟踪变化的方式
因为选择的答案不明确,所以我将把更正后的代码放在这里。请注意,在发生
DbUpdateException
之后,上下文如何从不被重用
public static async Task IncrementAsync(string key)
{
while (true)
{
using (var context = new MyContext())
{
var item = await context.Items.FindAsync(key);
if (item == null)
{
context.Items.Add(new Item { Key = key, Value = 1 });
}
else
{
item.Value++;
}
try
{
await context.SaveChangesAsync();
break;
}
catch (DbUpdateException)
{
continue;
}
}
}
}
您需要在尝试之间不共享上下文。如果可能,请不要对具有DbUpdateException
的上下文执行任何操作,因为如果不显式清理上下文,它可能永远不会返回正常状态
我希望外部环境会引起问题。如果对单个键的并发调用取决于时间,则可能会创建错误的上下文设置(由于错误处理程序,该设置将被不断忽略)
除非我弄错了,否则数据库中存在键
这一事实不会删除“待添加”版本。您将得到以下上下文之一:
Add "1", 2
或
这取决于您的第二次迭代是抓住第一次迭代的对象还是一个新对象
这两种方法都不可能成功,因此最终会出现一个连续的错误。给出答案的另一种方法是使用存储过程来完成所有工作,如以下示例所示。然后,您可以在应用程序中用一行代码而不是上面的代码来调用它
CREATE PROCEDURE SP_IncrementValue
@ValueKey NVARCHAR(100)
AS
BEGIN
BEGIN TRAN
UPDATE Item WITH (SERIALIZABLE) SET Value = Value + 1
WHERE [Key] = @ValueKey
IF @@ROWCOUNT = 0
BEGIN
INSERT Item ([Key], Value) VALUES (@ValueKey, 1)
END
COMMIT TRAN
END
GO
这种方法提供了更好的性能和更少的错误倾向
编辑:
要从C#调用存储过程,请在EF ApplicationDbContext类中添加以下方法
public int IncrementValue(string valueKey)
{
return this.Database.ExecuteSqlCommand("EXEC SP_IncrementValue @ValueKey", new SqlParameter("@ValueKey", valueKey));
}
然后您可以从DBContext类的任何实例调用它我会尝试一个较短的上下文,看看这是否有帮助。我必须四处玩,看看当并发更新发生时会发生什么情况,可能是某些不兼容的更新一起排队(最简单的示例是一个值不是1的add).您是否考虑过使用存储过程将同步卸载到SQL机器?@Guvante如果可能的话,我想留在纯EF的世界中(我可以在
和之间切换)除非像存储过程这样的操作真的是一个巨大的优化。这个特定的操作不会被大量调用。让它以适度的性能损失正常工作是可以接受的。如果您使用相同的键多次调用IncrementAsync
,则会有一个p问题,因为FindAsync
在转到数据库之前会在上下文中查找现有实体。您能否将如何从C#调用存储过程添加到这个答案中?
public int IncrementValue(string valueKey)
{
return this.Database.ExecuteSqlCommand("EXEC SP_IncrementValue @ValueKey", new SqlParameter("@ValueKey", valueKey));
}