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

时间:2012-09-04 23:19:33

标签: c# .net asp.net-mvc sql-server-2008 nhibernate

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

我将帖子简化为我想要的例子。也许这篇文章变得越来越长,吓跑了人们。

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

该示例需要做什么。

  1. 在mssql 2008中显示我的域中的数据类型应作为时间戳的版本
  2. 显示nhibernate自动抛出“StaleObjectException”
  3. 向我展示这3个场景的工作示例
  4. 情景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<TableA>().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<TableA>().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<TableA>())
                                 //  .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<TableA>
    {
        public TableAMapping()
        {
            Id(x => x.Id);
            Map(x => x.Name);
            Version(x => x.Version);
        }
    }
    

6 个答案:

答案 0 :(得分:6)

  

nhibernate会阻止该行被检索吗?

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

  

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

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

  

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

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

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

答案 1 :(得分:0)

是的,可以用nhibernate锁定一行,但是如果我理解得很好,那么你的场景就是在Web环境中,那么这不是最好的做法。

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

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

  

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

答案 2 :(得分:0)

我对nHibernate本身知之甚少,但如果你准备在数据库上创建一些存储过程,它可以&gt;排序&lt;完成。

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

  • 除散列字段本身和EditTimestamp字段之外的所有字段值的“散列”(使用SQL Server CHECKSUM 2008及更高版本或早期版本的HASHBYTES)。如果需要,可以使用INSERT / UPDATE触发器将其保留到表中。
  • datetime类型的'edit-timestamp'。

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

  • 'select'过程应包含类似于'edit-timestamp&lt;的where子句。 (现在 - 30分钟)'并且应该将'edit-timestamp'更新为当前时间。在更新行之前运行具有适当锁定的select我正在考虑使用保持锁定的存储过程,例如此here使用持久日期/时间而不是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 returns 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语句不会更新任何内容。我们得到'零行更新'的结果,因此我们知道该行已被更改或行被删除。

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

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

答案 3 :(得分:0)

你看过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<MyENtity>
{
    public MyEntityMap()
    {
        Table("MyTable");

        Id(x => x.ID);
        Version(x => x.Timestamp);
        Map(x => x.PropA);
        Map(x => x.PropB);

    }
}

答案 4 :(得分:0)

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

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

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

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

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

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

答案 5 :(得分:-1)