你如何在Nhibernate中进行版本控制?

我无法相信让某人向我展示一个简单的工作示例是如此困难。 它让我相信每个人只能说他们知道如何去做,但实际上他们不会。

我将post简化为我想要的例子。 也许这篇文章很长,吓跑了人们。

为了得到这个赏金,我正在寻找一个可以在VS 2010中复制并运行的工作示例。

这个例子需要做什么。

  1. 显示我的域中的数据类型应该是mssql 2008中的时间戳版本
  2. 显示nhibernate自动抛出“StaleObjectException”
  3. 向我展示这3个场景的工作示例

情景1

用户A进入该站点并编辑Row1。 用户B来(注意他可以看到Row1)并点击编辑Row1,在用户A完成之前,UserB应该被拒绝编辑该行。

场景2

用户A进入该站点并编辑Row1。 用户B出现30分钟后点击编辑Row1。 用户B应该能够编辑此行并保存。 这是因为用户A花了太长时间来编辑行并失去了编辑权。

场景3

用户A回来了。 他点击了更新行按钮,他应该受到StaleObjectException的欢迎。

我正在使用asp.net mvc和流利的nhibernate。 寻找在这些中完成的例子。


我尝试了什么

我试图建立自己的但我不能让它抛出StaleObjectException也不能让版本号增加。 我累了打开2个单独的浏览器并加载了索引页面。 两个浏览器都显示相同的版本号。

public class Default1Controller : Controller { // // GET: /Default1/ public ActionResult Index() { var sessionFactory = CreateSessionFactory(); using (var session = sessionFactory.OpenSession()) { using (var transaction = session.BeginTransaction()) { var firstRecord = session.Query().FirstOrDefault(); transaction.Commit(); return View(firstRecord); } } } public ActionResult Save() { var sessionFactory = CreateSessionFactory(); using (var session = sessionFactory.OpenSession()) { using (var transaction = session.BeginTransaction()) { var firstRecord = session.Query().FirstOrDefault(); firstRecord.Name = "test2"; transaction.Commit(); return View(); } } } private static ISessionFactory CreateSessionFactory() { return Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(c => c.FromConnectionStringWithKey("Test"))) .Mappings(m => m.FluentMappings.AddFromAssemblyOf()) // .ExposeConfiguration(BuidSchema) .BuildSessionFactory(); } private static void BuidSchema(NHibernate.Cfg.Configuration config) { new NHibernate.Tool.hbm2ddl.SchemaExport(config).Create(false, true); } } public class TableA { public virtual Guid Id { get; set; } public virtual string Name { get; set; } // Not sure what data type this should be for timestamp. // To eliminate changing to much started with int version // but want in the end timestamp. public virtual int Version { get; set; } } public class TableAMapping : ClassMap { public TableAMapping() { Id(x => x.Id); Map(x => x.Name); Version(x => x.Version); } } 

nhibernate会阻止该行被检索吗?

不会。锁定仅在事务范围内放置,在请求结束时Web应用程序中的事务处理结束。 此外,默认类型的事务隔离模式是Read committed ,这意味着只要select语句终止就会释放读锁定。 如果您正在同一个请求和事务中读取和编辑,则可以在手头的行上放置读写锁定,这将阻止其他事务写入或读取该行。 但是,这种类型的并发控制在Web应用程序中不能很好地工作。

或者用户B是否能够仍然看到该行,但如果他试图保存它会崩溃?

如果使用[乐观并发],就会发生这种情况。 在NHibernate中,乐观并发通过添加版本字段来工作 。 使用更新所基于的版本发出保存/更新命令。 如果这与数据库表中的版本不同,则不会更新任何行并且NHibernate将抛出。

如果用户A说取消并且不编辑,会发生什么。 我是否必须自己释放锁定,或者是否可以设置超时以释放锁定?

不,锁在请求结束时释放。

总的来说,您最好的选择是选择NHibernate管理的版本字段的乐观并发。

它在代码中看起来如何? 我是否在我的流畅的nhibernate中设置生成时间戳(不确定我是否会使用timepan数据类型)。

我建议使用版本列。 如果您正在使用带有自动映射的FluentNhibernate,那么如果您创建一个名为Version of int / long的列,默认情况下它将使用该版本,或者您可以使用映射中的Version()方法来执行此操作(它类似对于时间戳)。

所以现在我以某种方式生成了时间戳,用户正在编辑一行(通过gui)。 我应该将时间戳存储在内存中吗? 然后当用户从内存中提交调用行的时间戳和id并检查?

当用户开始编辑行时,您将检索它并存储当前版本(版本属性的值)。 我建议将当前版本放在表单中的隐藏字段中。 当用户保存更改时,您可以对数据库中的版本进行手动检查(检查它是否与隐藏字段中的版本相同),或者您可以将version属性设置为隐藏字段中的值(如果您正在使用数据绑定,则可以自动执行此操作)。 如果设置版本属性,那么当您尝试保存实体时,NHibernate将检查您保存的版本是否与数据库中的版本匹配,如果没有则抛出exception。

NHibernate将发出更新查询,例如:

UPDATE xyz SET,Version = 16 WHERE Id = 1234 AND Version = 15

(假设你的版本是15) – 在这个过程中它也会增加版本字段

如果是这样意味着业务逻辑正在跟踪“行锁定”,但理论上有人仍然可以去Where(x => x.Id == id)并抓住该行并随意更新。

如果其他人通过NHibernate更新了行,它将自动增加版本,所以当你的用户试图用错误的版本保存它时,你将得到一个例外,你需要决定如何处理(即尝试显示一些合并屏幕,或告诉用户再次使用新数据)

当行更新时会发生什么? 你将null设置为时间戳吗?

它会自动更新版本或时间戳(时间戳将更新为当前时间)

如果用户实际上从未完成更新并离开,会发生什么。 每一行如何再次解锁?

该行本身并未锁定,而是使用乐观并发,假设没有人会同时更改同一行,如果有人,则需要重试更新。

是否仍然存在竞争条件会发生什么事情或者这几乎不可能发生? 我只是担心2 ppl尝试编辑同一行,他们两个都在他们的gui中看到它进行编辑,但实际上最终会因为失去竞争条件而被拒绝。

如果2个人尝试同时编辑同一行,那么如果您使用乐观并发,其中一个将会丢失。 好处是他们会知道存在冲突,而不是失去他们的变化并认为它更新,或者在不知道它的情况下覆盖别人的变化。

所以我做了这样的事

var test = session.Query.Where(x => x.Id == id).FirstOrDefault(); //发送给用户进行编辑。 有版本控制。 用户编辑并在30分钟后发回数据。

代码呢

test.Id = vm.Id; test.ColumnA = vm.ColumnA; test.Version = vm.Version;

了Session.update(试验); session.Commit(); 所以上面的方法会正常吗?

如果其他人进入并更改了行,上面将抛出exception。 这就是它的重点,所以你知道出现了一个并发问题。 通常,您会向用户显示一条消息,说明“其他人已更改此行”,其中包含新行以及可能的更改,因此用户必须选择哪些更改获胜。

但如果我这样做

test.Id = vm.Id; test.ColumnA = vm.ColumnA;

 session.Update(test); session.Commit(); it would not commit right? 

只要你没有重新加载测试(即你测试=新的Xyz(),而不是test = session.Load()),因为行上的时间戳不匹配

如果其他人通过NHibernate更新了行,它将自动增加版本,所以当你的用户试图用错误的版本保存它时,你将得到一个例外,你需要决定如何处理(即尝试显示一些合并屏幕,或告诉用户再次使用新数据)

我可以在抓取此记录时进行此操作。 我想首先保持简单,一次只能有一个人编辑。 在编辑内容时,另一个人甚至无法访问要编辑的记录。

这不是乐观的并发。 作为一个简单的答案,您可以添加一个CheckOutDate属性,您可以在有人开始编辑时设置该属性,并在完成后将其设置为null。 然后,当他们开始编辑时,或者当您向他们显示要编辑的行时,您可以排除CheckOutDate比最近10分钟更新的所有行(然后您不需要计划任务来定期重置它)

该行本身并未锁定,而是使用乐观并发,假设没有人会同时更改同一行,如果有人,则需要重试更新。

我不确定你的意思是什么意思我能做到

session.query.Where(x => x.id == id).FirstOrDefault(); 整天都会让我记录下来(认为它会不断增加版本)。

查询不会增加版本,只有对它的更新才会增加版本。

我不太了解nHibernate本身,但如果你准备在数据库上创建一些存储过程,它可以>排序<完成。

您将需要一个额外的数据列和对象模型中的两个字段来存储每行的信息:

  • 除了哈希字段本身和EditTimestamp字段之外的所有字段值的“哈希”(使用SQL Server CHECKSUM 2008及更高版本或早期版本的HASHBYTES)。 如果需要,可以使用INSERT / UPDATE触发器将其持久保存到表中。
  • 日期时间类型的’edit-timestamp’。

更改您的过程以执行以下操作:

  • ‘select’过程应该包含类似于’edit-timestamp <(Now - 30 minutes)'的where子句,并且应该将'edit-timestamp'更新为当前时间。 在更新行之前运行具有适当锁定的select我正在考虑一个带有保持锁定的存储过程,例如此处使用持久日期/时间而不是像GETDATE()那样。

示例(使用固定值):

 BEGIN TRAN DECLARE @now DATETIME SET @now = '2012-09-28 14:00:00' SELECT *, @now AS NewEditTimestamp, CHECKSUM(ID, [Description]) AS RowChecksum FROM TestLocks WITH (HOLDLOCK, ROWLOCK) WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now) /* Do all your stuff here while the record is locked */ UPDATE TestLocks SET EditTimestamp = @now WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now) COMMIT TRAN 

如果你从这个程序中得到一行,那么你就'拥有''锁',否则,不会返回任何行,也没有什么可编辑的。

  • 'update'过程应该添加一个类似于'hash = previous = hash'的where子句

示例(使用固定值):

 BEGIN TRAN DECLARE @RowChecksum INT SET @RowChecksum = -845335138 UPDATE TestLocks SET [Description] = 'New Description' WHERE ID = 3 AND CHECKSUM(ID, [Description]) = @RowChecksum SELECT @@ROWCOUNT AS RowsUpdated COMMIT TRAN 

所以在你的场景中:

  1. 用户A编辑了一行。 当您从数据库返回此记录时,'edit-timestamp'已更新为当前时间,并且您有一行,因此您知道可以编辑。 用户B不会获得一行,因为时间戳仍然太新。

  2. 用户B在30分钟后编辑该行。 由于时间戳已超过30分钟,因此他们退回了一行。 由于没有写入更新,因此字段的哈希值与30分钟前的用户A相同。

  3. 现在用户B更新。 先前检索的哈希仍然匹配行中字段的哈希,因此更新语句成功,我们返回行计数以显示该行已更新。 但是,用户A尝试下次更新。 由于description字段的值已更改,因此hashvalue已更改,因此UPDATE语句不会更新任何内容。 我们得到'零行更新'的结果,因此我们知道该行已被更改或行被删除。

可能存在一些关于可扩展性的问题,所有这些锁都在进行,上面的代码可能会被优化(例如,可能会遇到时钟前进/后退问题,使用UTC),但我写这些例子只是为了解释它是如何工作的。

除此之外,如果不在select事务中使用数据库级行锁定,我无法看到如何做到这一点。 可能是你可以通过nHibernate请求这些锁,但是我害怕这超出了我对nHibernate的了解。

你看过ISaveOrUpdateEventListener接口了吗?

 public class SaveListener : NHibernate.Event.ISaveOrUpdateEventListener { public void OnSaveOrUpdate(NHibernate.Event.SaveOrUpdateEvent e) { NHibernate.Persister.Entity.IEntityPersister p = e.Session.GetEntityPersister(null, e.Entity); if (p.IsVersioned) { //TODO: check types etc... MyEntity m = (MyEntity) e.Entity; DateTime oldversion = (DateTime) p.GetVersion(m, e.Session.EntityMode); DateTime currversion = (DateTime) p.GetCurrentVersion(m.ID, e.Session); if (oldversion < currversion.AddMinutes(-30)) throw new StaleObjectStateException("MyEntity", m.ID); } } 

}

然后在您的配置中注册它。

  private static void Configure(NHibernate.Cfg.Configuration cfg) { cfg.EventListeners.SaveOrUpdateEventListeners = new NHibernate.Event.ISaveOrUpdateEventListener[] {new SaveListener()}; } public static ISessionFactory CreateSessionFactory() { return Fluently.Configure().Database(...). .Mappings(...) .ExposeConfiguration(Configure) .BuildSessionFactory(); } 

并在Mapping类中对要版本的属性进行版本控制。

 public class MyEntityMap: ClassMap { public MyEntityMap() { Table("MyTable"); Id(x => x.ID); Version(x => x.Timestamp); Map(x => x.PropA); Map(x => x.PropB); } } 

对你的问题的简短回答是你不能/不应该在具有nhibernates乐观(版本)和悲观(行锁)锁定的简单Web应用程序中执行此操作。 您的交易只与请求一样长,这是您的限制因素。

你可以做的是创建另一个表和实体类,以及管理这些“锁”的映射。 在最低级别,您需要正在编辑的对象的Id和执行编辑的用户的Id,以及获取锁定的日期时间。 我会将正在编辑的对象的Id作为主键,因为您希望它是独占的…

当用户单击要编辑的行时,您可以尝试获取锁(使用ids和当前日期时间在该表中创建新记录)。 如果另一个用户已经存在锁定,那么它将因为您尝试违反主键约束而失败。

如果获取了锁定,当用户单击“保存”时,您需要在执行实际保存之前检查它们是否仍具有有效的“锁定”。 然后,执行实际保存并删除锁定记录。

我还建议定期扫描这些锁定的后台服务/进程,并删除已过期或超过时间限制的锁定。

这是我在Web环境中处理“锁定”的规定方式。 祝好运!

是的,可以用nhibernate锁定一行,但是如果我理解的话,你的场景是在web上下文中,那么这不是最好的做法。

如上所述,最好的方法是使用带自动版本控制的乐观锁定。 在页面打开时锁定一行并在页面卸载时释放它将很快导致死锁(javascript问题,页面没有被正确杀死…)。 当刷新包含由另一个会话修改的对象的事务时,乐观锁定将使NHibernate抛出exception。 如果您希望对相同信息进行真正的concurent修改,您可能会尝试考虑在同一文档中合并许多用户输入的系统,但它本身就是一个系统,而不是由ORM管理。

您必须选择在Web环境中处理会话的方法。 http://nhibernate.info/doc/nh/en/index.html#transactions-optimistic

与高并发性和高可伸缩性一致的唯一方法是使用版本控制的乐观并发控制。 NHibernate提供了三种编写使用乐观并发的应用程序代码的方法。