【译】EF/EFCore 中RowVersion与ConcurrencyToken的比较
原文链接:传送门。
最近我被问到了一个相当好的关于EFCore的问题(虽然一般来说它并不是一个数据库的概念):我应该使用RowVersion 还是ConcurrencyToken作为乐观并发?
我觉得答案在于,更明确的说,你知道它们两者之间的区别及不足之处吗?
让我们往回倒一点,以准确的来说,什么是Concurrency Tokens来作为开始,然后是RowVersion,最后我们来看看它们是如何比较的。
什么是Concurrency Token
一个concurrency token 是一个每次一条数据库记录被更新时候都会进行检查的一个值。通过检查,我的意思更明确的说,就是已存在的值会被用作SQL语句的一部分,因此如果其在你这儿发生了变化,UPDATE语句便会失败。如果两个用户同时更新同一条记录,那么这种情况便会发生。
以非常粗糙的流程图形式表现如下:
用户B和用户A同时更新同一条记录时,最糟糕的情形他会将用户A的更新稀里糊涂的覆盖掉,就算是最好的情况,用户BY将用自己读取到的知识来对数据库所做的更新也会被用户A覆盖掉。
concurrency token 通过简单检查包含在最初读取中的信息是否还在(写入时)来对付这种情况。我们假设有一个叫做“User”的数据库表,其看起来像是这样:
Id int FirstName nvarchar(max) LastName nvarchar(max) Version int
通常一个没有带concurrency token的SQL Update语句看起来像是这样:
UPDATE User SET FirstName = 'Wade' WHERE Id = 1
但是如果我们用Version列作为concurrency token,它或许看起来像是这样:
UPDATE User SET FirstName = 'Wade', Version = Version + 1 WHERE Id = 1 AND Version = 1
在我们的Where语句中Version的值是我们读取最初的数据时获取到的值。在这种方式下,如果有个人在我们读取并更新数据的时候更新了这个记录,Version值便不会匹配,从而导致我们的更新失败。
在EF/EF Core中,我们有两种方式来指示一个属性是一个ConcurrencyToken,如果你更倾向于使用DataAnnotations你可以简单的在你的模型上应用一个特性【Attribute】:
[ConcurrencyCheck] public int Version { get; set; }
或者你更倾向于流式配置(应该使用这种方式),那么它会变得更加容易:
modelBuilder.Entity<People>() .Property(p => p.Version) .IsConcurrencyToken();
但是这有一个问题
因此所有的事情都听起来很棒。但是这有一个问题,一个小但却相当烦人的问题。
问题在于其缺少某种类型的数据库触发器,或者某种数据库自增字段,它取决于你,由开发者来保证每次进行Update操作时来增加Version。现在你显然可以编写一些EntityFramework扩展来解决这个问题,并在C#中自动增加内容,但它可以非常快速地复杂化。
RowVersion是什么
让我们以纯粹的SQL Server概念来开始什么是RowVersion。RowVersion(也被称为Timestamp,它们其实是一样的东西),是一个SQL 列类型,其使用在整个数据库内唯一的自增二进制数来标记记录。每次一个带有RowVersion的记录被插入或者更新到一个表中时,都会生成一个新的唯一的数字(以二进制的形式),并将其赋给那个记录。再一次的,RowVersions是整个数据库唯一的,而并不局限于这个表。
现在在EF/EF Core中这实际上是有一些不同的含义,这是由于SQL RowVersion用来实现的东西有所不同。
典型的在EF中,当某人说使用一个RowVersion时,他们其实是在说使用一个RowVersion/Timestamp列来用作一个“ConcurrencyToken”。现在如果你记得早些时候只是使用ConcurrencyToken带来的问题便是我们不得不自己update/increment 那个值,但是很显然如果SQL Server使用RowVersion来进行自动更新,那么问题便迎刃而解。
如果我们来看一看EF是如何计算是否使用一个RowVersion的,那么事情会变得更有意思。实际的代码可以查看这里:https://github.com/dotnet/efcore/blob/master/src/EFCore/Metadata/Builders/PropertyBuilder.cs#L152。
public virtual PropertyBuilder IsRowVersion() { Builder.ValueGenerated(ValueGenerated.OnAddOrUpdate, ConfigurationSource.Explicit); Builder.IsConcurrencyToken(true, ConfigurationSource.Explicit); return this; }
调用IsRowVersion()实际上是相当于告诉EFCore这个属性是ConcurrencyToken 并且是自动生成的。因此实际上,如果你手动给一个属性添加这两个配置。EF Core会将其看作一个RowVersion,即使你并不显示的说它是。
我们可以通过检查询问一个列是否是RowVersion的代码来看到这个:https://github.com/dotnet/efcore/blob/master/src/EFCore.Relational/Metadata/IColumn.cs#L56。
bool IsRowVersion => PropertyMappings.First().Property.IsConcurrencyToken && PropertyMappings.First().Property.ValueGenerated == ValueGenerated.OnAddOrUpdate;
所有实际上它所做的所有的工作就是审核一个列是否 是 concurrency token并且是自动生成的,简单吧!
我要指出的是,实际上如果你有一个列,你以某种其他的方式来递增(举个例子,数据库触发器),那么其也是一个concurrency token。我很确定EF Core处理这个会有一些问题,但那将是以后的某一天。
在EF中,你可以在一个属性上建立一个RowVersion,像是这样:
[TimeStamp] public byte[] RowVersion{ get; set; }
对于流式配置来说,其是这样:
modelBuilder.Entity<People>() .Property(p => p.RowVersion) .IsRowVersion();
即使你指定了一个列是一个RowVersion,其如何工作(数据类型,特定配置以及其如何更新)的实际实现实际上依赖于SQL SERVER。不同的数据库可以以其自己喜欢的方式来实现RowVersion。但是典型的在SQL SERVER中,其是byte[] 类型。
注意在EF中使用RowVersion,你真的不需要做其他更多的事情来建立和运行了。每次你更新带有RowVersion属性的一个记录时,它会自动将RowVersion属性添加到WHERE子句,为比提供了提供开箱即用的乐观并发性。
所以ConcurrencyToken还是RowVersion
所以让我们回过头来看看最初的那个问题:什么时候你应该使用 Concurrency Token,而什么时候你该使用RowVersion?答案其实非常简单。如果你想使用ConcurrencyToken作为一个自动增长的字段,并且你并不在乎其如何增长的以及其数据类型,那么便使用RowVersion。如果你在乎 concurrency token的数据类型应该什么,或者你想明确的控制其如何以及何时更新的,那么使用 Concurrency Token并自己管理其增长。
我通常发现,当人们建议我使用Concurrency Token时,他们实际上的意思是使用RowVersion。事实上我们很容易可以说:RowVersion(在EF)是一种特定类型的Concurrency Token.。