【问题标题】:Problems with NHibernate optimistic concurrency in a web application scenarioWeb应用场景中NHibernate乐观并发的问题
【发布时间】:2013-04-12 10:21:29
【问题描述】:

我正在使用版本字段来控制 ASP.NET MVC 4 应用程序中的并发性。

映射:

      <class name="User" table="Users">
        <id name="Id">
          <generator class="native"/>
        </id>
        <version name="Version" column="Version"/>
... other fields omitted for brevity...

实体:

    public class User
    {
            public virtual int Id { get; set; }
            public virtual int Version { get; set; }
... other fields omitted for brevity...

我正在使用以下方法:

  • 按 Id 读取实体并映射到我的 UserDto 实体(Dto 用于数据传输对象模式),还包括版本字段
  • 显示用于编辑 UserDto 实体的表单
  • 接收 POSTed UserDto 实体

然后我执行以下操作:

        // read the original entity from the database using my repository wrapper around NHibernate
        var rep = RepositoryFactory.Create<User>(currentUnitOfWork);
        User originalEntity = rep.GetById(userDto.Id);

        // optimistic lock control - keep the version as the user saw it
        originalEntity.Version = userDto.Version;
... other fields omitted for brevity...

        rep.Update(originalEntity);

问题在于,即使 userDto.Version 与 originalEntity.Version 不匹配,NHibernate 也会忽略我的 userDto.Version 并使用 originalEntity.Version(显然,来自一级缓存,因为刚刚读取了实体)。 这种行为使我的版本字段完全无用。

如何强制 NHibernate 使用我提供的版本值而不是缓存的值?

此外,以某种方式使版本控制对使用我的存储库的其他程序员更加透明会很棒,但目前我不知道如何让它自动从接收到的实体中获取版本并强制 NHibernate 使用它甚至没有程序员注意到它。

有什么想法吗?

【问题讨论】:

  • 您确定您的映射文件配置正确吗?它应该生成像UPDATE People SET ... WHERE PersonID = @p0 AND Version = @p1 这样的更新语句,其中@p1 = 用户最初读取的版本。见Ayendes post on NHibernate Concurrency
  • 是的,它确实会生成,但 Version 的值是它刚刚从数据库中读取的值,而不是我在 originalEntity.Version = userDto.Version 中手动设置的值。看来,NHibernate 只是忽略了我对 Version 字段的手动更新。如果我在更新行中断并更改数据库中的版本,那么我会得到 ObjectStaleStateException,正如预期的那样。

标签: c# nhibernate web-applications dto optimistic-concurrency


【解决方案1】:

您需要意识到,以下示例中的乐观并发仅在作为 this Web 请求的一部分创建的 ISession 范围内工作。因此,如果在此请求中 User 的 Version 值为 5,那么它将用于确保 User 行没有被更新。

    // read the original entity from the database using my repository wrapper around NHibernate
    var rep = RepositoryFactory.Create<User>(currentUnitOfWork);
    User originalEntity = rep.GetById(userDto.Id);

    // optimistic lock control - keep the version as the user saw it
    originalEntity.Version = userDto.Version;
    ... other fields omitted for brevity...

    rep.Update(originalEntity);

因此,要获得您需要做的所需行为

User originalEntity = rep.GetById(userDto.Id);
if (originalEntity.Version != userDto.Version) throw new ConcurrencyException();

正如documentation 中详述的那样,我希望您更愿意调用与以下内容松散对齐的东西

var @object = userDto.ToUser();
myisession.SaveOrUpdate(@object);

【讨论】:

  • 我希望依靠 NHib 为我做这件事。毕竟,NHibernate 会抛出 ObjectStaleStateException,如果我在 rep.Update 行上进行调试时修改数据库中的实体,所以我应该以某种方式告诉 NHib 使用我传递的版本......如果 NHiber 开发人员忘记了版本可能来自分离的对象。
  • 哦,感谢提供文档链接,现在我更好地理解了“11.4.4. 应用程序版本检查”一章的内容。遗憾的是 NHib 无法使用我自己的版本值,即使我明确设置它也是如此。我想,这可能是一个新的功能请求......
  • @Martin 你有没有找到更好的解决方案?这是 NHibernate 的一个严重缺陷。我仍然不相信我将不得不手动执行此操作?这肯定是常见的情况吗?
【解决方案2】:

我知道这是一个老问题,但我会在这里保留我通常的方法。

这是使用 ORM 时已知的“问题”。 NHibernate 和实体框架都遇到了这个“问题”,这是因为 ORM 在内部跟踪版本值,而不是使用属性返回的值。与 EF 不同,您可以使用 Array.Copy 复制 byte[] 值,在 NHibernate 中,我通常将实体从会话中逐出,然后进行更新,这向 NHibernate 表明您正在更新现有实体,但他将使用您刚刚分配的版本开始跟踪。

这是一个代码sn-p:

public void Edit(int id, string description, DateTime version)
{
    using (var session = sessionFactory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var record = session.Get<Record>(id);
        record.Version = version;
        record.Description = description;

        session.Evict(record);  //  evict the object from the session
        session.Update(record); //  NHibernate will attach the object, and will use your version

        tx.Commit();
    }
}

如果你像我通常在我的模型类中那样使用接口 (example),你可以轻松创建一些扩展方法,让人们更难忘记。

据我所知,我没有发现这种方法有任何问题,但如果您发现任何问题,请告诉我。

【讨论】:

    【解决方案3】:

    你可以看看@this

    但是,由于拦截器是旧概念,您可以在自定义事件侦听器中扩展 DefaultUpdateEventListener 并覆盖 OnSaveOrUpdate 方法,如下所示

    var source = @event.Session;
    var entity = @event.Entity;
    var persister = @event.Session.GetEntityPersister(@event.EntityName, entity);
    if (persister.IsVersioned)
    {
        var mode = source.GetSessionImplementation().EntityMode;
        var id = persister.GetIdentifier(entity, mode);
    
        var version = persister.GetVersion(entity, mode);
        var currentVersion = persister.GetCurrentVersion(id, source);
    
        if (!version.Equals(currentVersion))
        {
            throw new StaleObjectStateException(persister.EntityName, id);
        }
    }
    

    这里唯一令人头疼的是 GetCurrntVersion 发送一个 DB 调用。因此,这不是一个非常适合云应用程序的解决方案,因为每次通话都要计费。

    或者,您可以从会话中逐出对象。但是,您将失去一级缓存优势。

    如果我找到更好的解决方案,会发布。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-30
      相关资源
      最近更新 更多