我有以下代码,我试图使用ef核心更新ClientAccount,但它们的并发检查失败:
public class ClientAccount
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[ConcurrencyCheck]
public double Balance { get; set; }
[Required]
public DateTime DateTimeCreated { get; set; }
[Required]
public DateTime DateTimeUpdated { get; set; }
}
public class ClientRepository
{
private readonly MyContext context;
public ClientRepository(MyContext context)
{
this.context = context;
}
public ClientAccount GetClientAccount()
{
return (from client in context.ClientAccount
select client).SingleOrDefault();
}
public void Update(ClientAccount client)
{
context.Update(client);
context.Entry(client).Property(x => x.DateTimeCreated).IsModified = false;
}
}
public class ClientService
{
private readonly ClientRepository clientRepository;
private readonly IUnitOfWork unitOfWork;
public ClientService(ClientRepository clientRepository,
IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
this.clientRepository = clientRepository;
}
public void Update(ClientAccount clientAccount)
{
if (clientAccount == null)
return;
try
{
ClientAccount existingClient = clientRepository.GetClientAccount();
if (existingClient == null)
{
// COde to create client
}
else
{
existingClient.AvailableFunds = clientAccount.Balance;
existingClient.DateTimeUpdated = DateTime.UtcNow;
clientRepository.Update(existingClient);
}
unitOfWork.Commit();
}
catch (DbUpdateConcurrencyException ex)
{
}
}
}
问题是,当两个线程同时尝试更新它时,DbUpdateConcurrencyException
不会被触发,因此我没有预期的功能。
我不明白这里有什么问题,因为使用ConcurrencyCheck属性标记属性应该可以正常工作。
答案 0 :(得分:3)
无法按预期工作
确实如此,但您的代码几乎不会引起并发异常。
在Update
方法中,从数据库中提取现有客户端,进行修改并立即保存。当从数据库中新鲜出来时,客户端(显然)具有最新值Balance
,而不是它进入UI时的值。整个操作是一个毫秒的问题,其他用户在很短的时间内保存同一个客户端的可能性很小。
如果要显示并发冲突,则应将原始值存储在ClientAccount
对象中,并将其分配给上下文中的原始值。例如:
班级:
public class ClientAccount
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[ConcurrencyCheck]
public double Balance { get; set; }
[NotMapped]
public double OriginalBalance { get; set; }
...
}
在更新方法中,为简洁起见,我们在那里提供了上下文:
ClientAccount existingClient = db.ClientAccount.Find(clientAccount.Id);
db.Entry(existingClient).OriginalValues["Balance"] = clientAccount.OriginalBalance;
existingClient.Balance = clientAccount.Balance; // assuming that AvailableFunds is a typo
db.SaveChanges();
您还需要在用户编辑的对象中设置OriginalBalance
。由于您使用存储库,因此必须添加一个方法,将原始值提供给包装上下文。
现在所有这些只适用于一个属性。更常见的是使用一个特殊属性进行乐观并发控制,一个"版本" property - 或数据库中的字段。某些数据库(其中Sql Server)会在每次更新时自动增加此版本字段,这意味着当记录的任何值已更新时,它将始终不同。
所以让你的课有这个属性:
public byte[] Rowversion { get; set; }
映射:
modelBuilder.Entity<ClientAccount>().Property(c => c.Rowversion).IsRowVersion();
(或使用[System.ComponentModel.DataAnnotations.Timestamp]
属性)。
现在,您可以简单地执行...
,而不是存储原始余额并在以后使用它db.Entry(existingClient).OriginalValues["Rowversion"] = clientAccount.Rowversion;
...并且用户将了解任何并发冲突。
您可以在EF-core here中阅读有关并发控制的更多信息,但请注意(令人惊讶的是)他们错误地使用IsConcurrencyToken()
而不是IsRowVersion
。这导致了不同的行为,正如我为EF6描述的here,但它仍适用于EF-core。
using (var db = new MyContext(connectionString))
{
var editedClientAccount = db.ClientAccounts.FirstOrDefault();
editedClientAccount.OrgBalance = editedClientAccount.Balance;
// Mimic editing in UI:
editedClientAccount.Balance = DateTime.Now.Ticks;
// Mimic concurrent update.
Thread.Sleep(200);
using (var db2 = new MyContext(connectionString))
{
db2.ClientAccounts.First().Balance = DateTime.Now.Ticks;
db2.SaveChanges();
}
Thread.Sleep(200);
// Mimic return from UI:
var existingClient = db.ClientAccounts.Find(editedClientAccount.ID);
db.Entry(existingClient).OriginalValues["Balance"] = editedClientAccount.OrgBalance;
existingClient.Balance = editedClientAccount.Balance;
db.SaveChanges(); // Throws the DbUpdateConcurrencyException
}
这是上次更新执行的SQL:
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [ClientAccount] SET [Balance] = @p0
WHERE [ID] = @p1 AND [Balance] = @p2;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 float,@p2 float',@p1=6,@p0=636473049969058940,@p2=1234
答案 1 :(得分:0)
将[Timestamp]
属性添加到DateTimeUpdated
。应该在那之后工作,但不可否认,我还没有使用过这个功能。我认为类型应该是byte[]
。
找到参考链接:https://docs.microsoft.com/en-us/ef/core/modeling/concurrency#timestamprow-version