数据访问

使用 NHibernate 生成桌面任务管理应用程序

Oren Eini

下载代码示例

NHibernate 是对象关系映射 (OR / M),以使其易于使用的数据库,因为它以使用内存中的对象作为 tasked。它是一个最受欢迎的 OR / M Microsoft.net 框架开发的框架。但是,大多数用户 NHibernate 的这样的 Web 的应用程序上下文中因此有关构建 NHibernate 在桌面上的应用程序相对较少信息。

在 Web 应用程序中使用 NHibernate 时, 我倾向于使用会话每个请求为样式中有很多,很容易错过的含义。因为我所期望的会话将消失不久我 don’t 担心会话维护对已加载的实体的引用。我 don’t 不必再担心错误处理 (很多),如我可以只是中止当前的请求和与其相关联的会话如果发生错误。

会话的生存期是完善定义的。don’t 需要更新有关所做的任何更改其他会话。don’t 不必担心会在数据库中保持长事务或甚至保持连接打开很长的时间,因为它们的单个请求持续时间内才处于活动状态。

可以想象那些都是桌面应用程序中的问题。只是为了进行清除有关该,I am 谈论与数据库直接交谈的应用程序。使用某种类型的远程服务的应用程序在每个请求方案下的在远程服务器上使用 NHibernate 并不是本文的重点。本文未涵盖偶尔连接的情况下,尽管此处讨论的大部分内容将应用于这些方案。

构建基于 NHibernate 的桌面应用程序 isn’t 比构建桌面应用程序使用任何其他持久性技术得不同。许多挑战我打算在本文中分级显示的所有数据访问技术之间共享:

  • 管理工作单元的作用域。
  • 减少打开的数据库连接的持续时间。
  • 传播实体将更改为应用程序的所有部分。
  • 支持双向数据绑定。
  • 减少启动时间。
  • 避免阻塞用户界面线程访问数据库时。
  • 处理和解决并发冲突。

为我提供的解决方案在处理以独占方式 NHibernate,至少的大多数时它们都还适用于其他数据访问技术。一个类挑战,共享的所有我知道的的数据访问技术是如何管理应用程序已经工作单元的作用域 — 或在 NHibernate 已经术语会话寿命。

管理会话

常见的错误做法与 NHibernate 桌面应用程序是具有单一全局会话,对整个应用程序。出于许多原因是一个问题,但它们中的三个是最重要。由于一个会话保留一个引用到它加载的所有内容,从事应用程序用户将要加载实体、 与它的一个位工作,然后忘。但是,因为单个全局会话正在维护对它的引用,永远不会释放该实体。在实质的方式上,您在应用程序中有内存泄漏。

然后是错误处理的问题。获取异常 (如 StaleObjectStateException,由于并发冲突的),如果您的会话和它加载的实体是祝酒,因为 NHibernate,与从会话中引发的异常该会话移动到未定义的状态。您不再可以使用该会话或任何加载的实体。如果只有单个全局会话它意味着您可能需要重新启动该应用程序可能不是一个好主意。

最后,’s 事务和连接处理的问题。虽然打开会话 isn’t 无异到打开数据库连接,使用单个会话意味着更可能要容纳交易记录和连接时间超过您应该。

另一个同样错误和,遗憾的是,几乎公练习所用 NHibernate 是 micromanaging 的会话。一个典型示例是类似下面的代码:

public void Save(ToDoAction action) {
  using(var session = sessionFactory.OpenSession())
  using(var tx = session.BeginTransaction()) {
    session.SaveOrUpdate(action);

    tx.Commit();
  }
}

这种类型存在的问题是代码的它使用 NHibernate 中删除了大量所获得的优点。NHibernate 这样做相当位来处理更改管理和透明持久性为您的工作。Micromanaging 会话关闭 NHibernate 已经能够做到这一点剪切并将移动的工作以您这样做 onus。并且,即使没有它将导致与惰性加载行下移问题一提的是。我已经看到过这种方法尝试任何系统,在开发人员必须困难工作以解决这些问题。

会话应该不会保持打开时间太长,但它应该也不能保持打开以使太短时间 NHibernate 已经功能的使用。通常,请尝试以匹配实际由系统正在执行的操作以会话寿命。

对于桌面应用程序建议的做法是使用窗体,每一个会话,以便在应用程序中的每个窗体都有其自己的会话。每个窗体通常代表用户想要执行,以便匹配到窗体生命周期的会话生存期适用相当于练习一个不同的一段的工作。额外的好处是因为当您关闭该应用程序窗体中的,您还释放会话的您不再有内存泄漏问题。这将使会话符合垃圾回收器 (GC) 回收的条件由已加载的所有实体。

有其他原因需要首选窗体每一个会话。因此,它将刷新到数据库的所有更改提交事务时,您可以充分利用 NHibernate 已经更改跟踪。它还创建不同表单以便您可以更改提交到单个实体而不必担心更改到其他窗体显示的其他实体之间的隔离屏障。

管理会话生存期的此样式说明为窗体每一个会话时, 在练习中,通常是管理每演示者会话。在 的 图 1 中代码取自演示者的基类。

图 1 的 演示者会话管理

protected ISession Session {
  get {
    if (session == null)
      session = sessionFactory.OpenSession();
    return session;
  }
}

protected IStatelessSession StatelessSession {
  get {
    if (statelessSession == null)
      statelessSession = sessionFactory.OpenStatelessSession();
    return statelessSession;
  }
}

public virtual void Dispose() {
  if (session != null)
    session.Dispose();
  if (statelessSession != null)
    statelessSession.Dispose();
}

您可以看到我 lazily 打开一个会话 (或无状态的会话) 并使其保持打开状态直到我的演示者处置。此样式到窗体本身的生命周期非常很好地匹配,并允许我将与每个演示者关联一个单独的会话。

维护连接

该演示 don’t 不必担心打开或关闭该会话。第一次您访问一个会话为您,打开它,它将释放本身也正确。但是,什么是数据库连接与会话相关联?是否您持有一个数据库连接打开的只要用户正在查看窗体吗?

大多数数据库 dislike 不必容纳事务打开的时间过长。它通常会导致引发错误或下移行的死锁。已打开的连接会导致类似的问题,因为一个数据库只能接受如此多的连接之前用尽资源来处理该连接。

若要最大化您数据库服务器的性能,应保持最小,并尽快,关闭连接的事务寿命依赖连接池以确保当您打开一个新的连接时的快速响应时间。

NHibernate,这种情况将更相同,只不过 NHibernate 包含有要显式使操作更容易为您的几种功能。NHibernate 会话 doesn’t 具有与数据库连接一对一关联。而是,NHibernate 管理数据库连接在内部,打开和关闭它,根据需要。这意味着您 don’t 具有维护某种类型的应用程序断开连接,并根据需要重新连接到数据库中的状态。榛樿鎯呭喌涓嬶,NHibernate 将最小化最大持续时间内连接处于打开状态。

您不必担心会使事务尽可能小。在具体的方式而言要 don’t 的事情之一是存放一个交易记录窗体的生存期内打开。这将强制保持事务的持续时间为连接打开的 NHibernate。然后,因为窗体的生存期开始测量,以人为的响应时间,’s 多个可能是您将得到按住向上事务和长比真正运行状况良好的连接。

您将通常是做什么是打开单独的交易记录所做的每个操作。let’s 看显示简单的待办事项列表我的示例应用程序所选择的一个窗体模型 (请参阅 的 图 2)。正如您所看到的 的 图 3 中,处理此窗体的代码是非常简单。

图 2 的 A 待办事项列表应用程序

图 3 创建待办事项窗体

public void OnLoaded() {
  LoadPage(0);
}

public void OnMoveNext() {
  LoadPage(CurrentPage + 1);
}

public void OnMovePrev() {
  LoadPage(CurrentPage - 1);
}

private void LoadPage(int page) {
  using (var tx = StatelessSession.BeginTransaction()) {
    var actions = StatelessSession.CreateCriteria<ToDoAction>()
      .SetFirstResult(page * PageSize)
      .SetMaxResults(PageSize)
      .List<ToDoAction>();

    var total = StatelessSession.CreateCriteria<ToDoAction>()
      .SetProjection(Projections.RowCount())
      .UniqueResult<int>();

    this.NumberOfPages.Value = total / PageSize + 
               (total % PageSize == 0 ? 0 : 1);
    this.Model = new Model {
      Actions = new ObservableCollection<ToDoAction>(actions),
      NumberOfPages = NumberOfPages,
      CurrentPage = CurrentPage + 1
    };
    this.CurrentPage.Value = page;

    tx.Commit();
  }
}

我有三个操作:第一次显示第一页和记录间来回分页加载窗体。

每个操作在我开始,提交一个单独的事务。这种方式我 don’t 消耗数据库上的任何资源,don’t 不必再担心长的交易记录。开始新事务和事务完成后关闭它时,NHibernate 将自动打开到数据库连接。

无状态的会话

我应该注意的另一个小 subtlety 不存在:我不使用一个 ISession。而,我使用在其位置 IStatelessSession 加载数据。大容量数据操作中, 通常使用无状态的会话,但我进行这种情况下使用的无状态的会话来解决内存消耗问题。

无状态的会话,嗯,无状态。与普通的会话不同它 doesn’t 维护对它加载的实体的引用。为这样它完全适合于仅显示用于加载实体。用于该任务类型您通常从数据库加载实体、 窗体上引发它们和忘记有关它们。无状态的会话是正是您需要在这种情况下。

但是,无状态的会话都附带一组限制。在这种情况下,在它们之间的首席无状态的会话不支持惰性加载、 不平常的 NHibernate 事件模式涉及本身而不进行使用 NHibernate 的缓存功能。

出于这些原因通常使用它们的简单查询其中我只想显示而不做任何复杂的用户的信息。在想要显示用于编辑实体的情况下,我可能仍然使无状态的会话的使用,但我倾向于普通的会话而避免的。

在主应用程序窗体中我努力使所有数据,只显示的试图都进行的单独的无状态会话使用。只要应用程序处于打开状态,只是因为它将会维护对它加载的实体引用不是一个问题,但因为 ’s 可能导致问题,如果该会话将引发异常将有状态的会话,主窗体所居住的。

NHibernate 认为遇到异常情况 (只 Dispose 具有已定义的行为在这种情况下) 中未定义的状态是一个会话。您需要替换该会话,因为实体加载通过监控状态的会话维护对它的引用,就需要清除由现在已失效的会话已加载的所有实体。它 ’s 因此变得更加简单在这种情况下使用无状态的会话。

加载通过无状态的会话的实体执行不关心该会话的状态和无状态的会话中从错误中恢复与关闭当前无状态的会话和打开一个新一样简单。

操作数据

图 4 显示编辑屏幕模型。哪些挑战做您面临时需要编辑实体?

图 4 编辑实体

嗯,实际上有两个单独的挑战此处。要进行的第一次,能够利用 NHibernate 更改跟踪,可以显示某个实体 (或实体对象图) 和只具有 NHibernate 它时保存 ’re 完成。其次,一旦保存某个实体时,您需要确保还会显示此实体的每个表单更新用新值。

业务的第一项是以处理实际上相当简单。您需要做的就是使该表单相关联的会话使用和的 ’s。图 5 显示此屏幕的行车代码。

图 5 编辑会话中的一个实体

public void Initialize(long id) {
  ToDoAction action;
  using (var tx = Session.BeginTransaction()) {
    action = Session.Get<ToDoAction>(id);
    tx.Commit();
  }

  if(action == null)
    throw new InvalidOperationException(
      "Action " + id + " does not exists");

  this.Model = new Model {
    Action = action
  };
}

public void OnSave() {
  using (var tx = Session.BeginTransaction()) {
    // this isn't strictly necessary, NHibernate will 
    // automatically do it for us, but it make things
    // more explicit
    Session.Update(Model.Action);

    tx.Commit();
  }

  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  View.Close();
}

从 Initialize(id) 方法在数据库中获取该实体和更新 OnSave 方法中。请注意,而不是保持事务处于活动状态的时间较长的两个单独事务中这样做。此外 ’s 此奇怪 EventPublisher 调用。什么 ’s 的有关所有?

EventPublisher 这里是要处理的另一个挑战:当每个窗体及其会话时每个窗体都有不同的处理与实体的 
instances。表面上它,如下所示浪费了。为什么应该在加载相同的操作几次?

在 actuality,遇到这种分离该窗体之间将简化应用程序有很大。请考虑将发生什么情况如果您在整个棋盘上共享的实体实例。在该情况中您会发现自己问题与在任何 conceivable 编辑方案。请考虑将发生什么情况如果您要允许编辑该实体的两个窗体中显示的实体。这可能是可编辑的网格和详细的编辑窗体例如。如果将更改连接到实体在网格中打开详细的编辑表单并保存它以可编辑的网格所做的更改会发生什么?

如果采用单个实体实例,在整个应用程序,然后它 ’s 可能保存详细信息窗体还将导致您保存使用网格所做的更改。’s 很可能不想做的事情。共享实体实例也使其执行操作,如取消编辑表单并将所有未保存的更改消失会变得更加困难。

这些问题只是不存在时使用实体实例每是一个很好的事情,如使用窗体的方法的每个会话时,这是强制性更多或更少的表单。

发布事件

但我完全 haven’t 还解释该 EventPublisher 的目的。它 ’s 实际上相当简单。而不是实体的单个实例应用程序中,您可能有许多,但用户仍希望看到在显示该实体的所有窗体上更新该实体 (一次正确保存)。

在我的示例中我这样做显式。每当保存实体发布说我确实因此,并在哪个实体上的事件。这 isn’t 标准的.net 事件。.NET 事件要求要直接订阅类。实际 doesn’t 适合这种类型的通知,因为它将需要在所有其他窗体中的事件注册每个窗体。只尝试管理的将是一个任务。

在 EventPublisher 是发布-订阅耦合其订阅者从发行者,我使用的机制。它们之间唯一的通用性是 EventPublisher 类。我可以使用事件类型 (在 的 图 5 中的 ActionUpdated) 决定谁告知有关该事件。

let’s 查看的另一侧现在。当我更新待办事项操作时,我希望在主要的表单显示待办事项操作的网格中显示更新后的值。以下是从该窗体演示者相关的代码:

public Presenter() {
  EventPublisher.Register<ActionUpdated>(
    RefreshCurrentPage);
}

private void RefreshCurrentPage(
  ActionUpdated actionUpdated) {
  LoadPage(CurrentPage);
}

在启动时,我注册方法 RefreshCurrentPage ActionUpdated 事件。现在,每当引发该事件时,将只是刷新当前页是在您已熟悉的调用 LoadPage 情况。

这是实际上相当惰性实现。如果当前网页正在显示编辑过的实体,我 don’t 关心 ; 只需刷新它仍要。如果在该页上显示更新后的实体,更复杂 (和有效) 的实现将只刷新网格数据。

使用该发布的主要优点-订阅在这种机制发行者和订户的方式通过分隔开来。我 don’t 关心编辑窗体发布 ActionUpdated 事件与主窗体中。事件发布的思想和发布-订阅是在构建松散基础耦合的用户界面和复合应用指南 ( msdn.microsoft.com/library/cc707819 ) 中从 Microsoft 模式与做法团队广泛地介绍。

没有值得考虑的另一种情况:如果您有两个编辑窗体同时打开相同的实体,则会发生什么情况?如何可以从数据库获取新值并向用户显示它们吗?

下面的代码取自编辑窗体演示者:

public Presenter() {
  EventPublisher.Register<ActionUpdated>(RefreshAction);
}

private void RefreshAction(ActionUpdated actionUpdated) {
  if(actionUpdated.Id != Model.Action.Id)
    return;
  Session.Refresh(Model.Action);
}

此代码注册为 ActionUpdated 事件和它 ’s 编辑该实体,如果您询问以从数据库刷新它 NHibernate。

刷新数据库从实体的此显式模型还使您有机会作出有关应发生现在。应更新自动,擦除所有用户的更改吗?您应该询问用户吗?请尝试以静默方式合并所做的更改吗?这些就是您现在有一个简单的方式处理的机会的所有决策。

大多数的情况下但是,我发现只需刷新该实体是相当足够,因为您通常不允许的单个实体 (至少不由单个用户) 的并行更新。

虽然此实体刷新代码实际上将更新实体实例的值,您将如何进行此更改 UI 响应?绑定到窗体域的实体值的数据,但您需要告诉这些值已更改的用户界面的一些方式。

Microsoft.net 框架提供的大多数 UI 框架了解并知道如何处理在 INotifyPropertyChanged 界面。在此处 ’s INotifyPropertyChanged 定义:

public delegate void PropertyChangedEventHandler(
  object sender, PropertyChangedEventArgs e);

public class PropertyChangedEventArgs : EventArgs {
  public PropertyChangedEventArgs(string propertyName);
  public virtual string PropertyName { get; }
}

public interface INotifyPropertyChanged {
  event PropertyChangedEventHandler PropertyChanged;
}

实现此接口的对象应引发 PropertyChanged 事件与已更改的属性的名称。在用户界面将订阅 PropertyChanged 事件,无论何时更改引发绑定的属性上,它将刷新绑定。

实现这是相当容易:

public class Action : INotifyPropertyChanged {
  private string title;
  public virtual string Title {
    get { return title; }
    set {
      title = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Title"));
    }
  }

  public event PropertyChangedEventHandler 
    PropertyChanged = delegate { };
}

简单,时它 ’s 相当重复性代码,只需满足 UI 基础结构问题。

截取实体创建

don’t 希望必须编写代码只是为了获得正常工作的用户界面数据绑定。 并且,事实证明,don’t 实际需要。

NHibernate 的要求之一是您做出的所有属性和方法在您的类上虚拟。 NHibernate 要求这正确地,处理惰性加载关心的问题,但您可以利用这种要求出于其他原因。

您可以执行的一件事情是利用虚拟关键字来组合成插入您自己的行为。 您这样使用一种技术称为 Aspect-Oriented 编程 (AOP)。 在实质的方式上采用一个类并向此类在运行时添加其他行为。 确切的机制的方式实现这超出了该文章的范围,但它封装在其定义为 DataBindingFactory 类别类别中:

public static class DataBindingFactory {
  public static T Create<T>();
  public static object Create(Type type);
}

整个类的实现是大约 40 行不特别复杂。 它的作用是采取一种类型,并产生也完全实现 INotifyPropertyChanged 合同此类型的实例。 也就将工作以下测试:

ToDoAction action = DataBindingFactory.Create<ToDoAction>();
string changedProp = null;
((INotifyPropertyChanged)action).PropertyChanged 
  += (sender, args) => changedProp = args.PropertyName;
action.Title = "new val";
Assert.Equal("Title", changedProp);

给定的您只需现在是使演示者在创建新类时使用的该 DataBindingFactory。从这样的系统中获得的主要优点是,现在,如果您想让使用 NHibernate 域模型中非演示文稿上下文的、 可以只是不使该 DataBindingFactory 的使用和获取域模型完全免费从演示文稿关心的问题。

有 ’s 仍一个问题但。虽然您可以创建 的实体的 实例使用 DataBindingFactory,时间的很多您具有处理创建 NHibernate 的实例。显然,NHibernate 知道没有任何有关您 DataBindingFactory 和 can’t 使它的使用。但您灰心之前,您可以使一个最有用的扩展点,与 NHibernate,该侦听器的使用。NHibernate 已经侦听器可以接管,实质中, 某些在内部执行 NHibernate 的功能。

该侦听器可以接管该功能之一创建新实例的实体。图 6 显示了一个侦听器,可创建使用该 DataBindingFactory 的实体的实例。

图 6 截取实体创建

public class DataBindingInterceptor : EmptyInterceptor {
  public ISessionFactory SessionFactory { set; get; }

  public override object Instantiate(string clazz, 
    EntityMode entityMode, object id) {

    if(entityMode == EntityMode.Poco) {
      Type type = Type.GetType(clazz);
      if (type != null) {
        var instance = DataBindingFactory.Create(type);
        SessionFactory.GetClassMetadata(clazz)
          .SetIdentifier(instance, id, entityMode);
        return instance;
      }
    }
    return base.Instantiate(clazz, entityMode, id);
  }

  public override string GetEntityName(object entity) {
    var markerInterface = entity as
      DataBindingFactory.IMarkerInterface;
    if (markerInterface != null)
      return markerInterface.TypeName;
    return base.GetEntityName(entity);
  }
}

重写实例化方法,并处理这种情况,我们实体与所识别的类型。 然后继续创建类的实例并将其标识符属性设置。 您还需要教 NHibernate 如何理解 DataBindingFactory 通过创建一个实例属于什么类型的该 intercepter GetEntityName 方法中执行该操作。

现在离开该唯一一件事就是设置与新的侦听器 NHibernate。 下面取自负责设置应用程序在引导程序类:

public static void Initialize() {
  Configuration = LoadConfigurationFromFile();
  if(Configuration == null) {
    Configuration = new Configuration()
      .Configure("hibernate.cfg.xml");
    SaveConfigurationToFile(Configuration);
  }
  var intercepter = new DataBindingIntercepter();
  SessionFactory = Configuration
    .SetInterceptor(intercepter)
    .BuildSessionFactory();
  intercepter.SessionFactory = SessionFactory;
}

现在,忽略配置语义 — 我将解决的一个位中。重要的一点是创建该侦听器、 设置上配置和生成会话工厂。最后一步设置会话工厂上该侦听器。它 ’s 有点麻烦我承认,但的 ’s 适当会话工厂进入该侦听器最简单的方法。

一旦该侦听器有线,NHibernate 创建每个实体实例将现在支持 INotifyPropertyChanged 通知而不必在所有执行任何工作。我认为这相当的典雅型问题解决方案。

有会说选择这样的解决方案是从性能角度看问题通过硬编码实现几个。在练习中的证明是假的假设。为确保最佳性能,已经很大程度优化我来执行此上动态扩展的类的使用 (Castle 动态代理服务器) 的工具。

寻址的性能

说到性能,您没有在 Web 应用程序中的桌面应用程序中的其他考虑因素是启动时间。在 Web 应用程序中是很常见决定优选较长的启动时间,以增加请求的性能。在桌面的应用程序中想要减少启动时间尽可能多地。在事实的方式数据表与桌面应用程序公共作弊是启动应用程序完成之前只需显示给用户应用程序的一个屏幕快照。

遗憾的是,NHibernate 启动时间是有点长。这是初始化的主要是初始化的因为 NHibernate 正在执行大量,并且在启动时,检查,以便它可以更快地执行正常操作期间。有两种处理此问题的常见方式。

第一个是在后台线程启动 NHibernate。虽然这意味着在用户界面将会显示很多更快地,它还创建 complication 
for 应用程序本身,因为您 can’t 显示用户从数据库中的任何内容直到完成会话工厂启动。

另一种选择是要序列化 NHibernate 已经配置类。验证传递给配置类信息的成本与大量与 NHibernate 启动相关的成本。配置类是可序列化的类,因此您可以支付一次该价格其之后您可以通过从永久性存储中加载已经被验证的实例快捷成本。

这就是 LoadConfigurationFromFile 和 SaveConfigurationToFile 序列化和反序列化 NHibernate 已经配置的目的。使用这些只需创建配置第一次启动应用程序。但有 ’s 应该注意的一个小问题:如果实体程序集或 NHibernate 配置文件已更改,您应使无效缓存的配置。

本文的代码示例包含一个完整实现的 ‘ s 注意这一点并使缓存的文件无效,如果该实体或配置已更改。

有 ’s 必须处理的另一个性能问题。调用该数据库是一种更昂贵的操作使应用程序。为这样它 ’s 不要在应用程序 UI 线程上执行的某些内容。

这样的职责通常不为后台线程,您可以执行同样的 NHibernate,但请记住 NHibernate 会话不是线程安全。虽然您可以进行一个会话中 (它有没有线程关联) 的多个线程的使用,不能使用一个会话 (或您的实体) 上并行的多个线程。也就 ’s 完全正常使用后台线程中的会话,但您必须对其进行序列化到该会话的访问和不允许对它的并发访问。并行使用多个线程从会话会导致未定义的行为 ; 也就应该避免。

幸运的是,是为确保访问到该会话已序列化可以采取的几个相对简单的度量值。System.ComponentModel.BackgroundWorker 类旨在显式处理这些排序的任务。它允许您在后台线程上执行的任务,并通知您它完成时,小心 UI 线程同步问题,这是在桌面应用程序中非常重要。

您以前看到过如何管理编辑垜杩涜直接在 UI 线程的现有实体。现在,let’s 保存在后台线程上的新实体。下面的代码是创建新的演示者的初始化:

private readonly BackgroundWorker saveBackgroundWorker;

public Presenter() {
  saveBackgroundWorker = new BackgroundWorker();
  saveBackgroundWorker.DoWork += 
    (sender, args) => PerformActualSave();
  saveBackgroundWorker.RunWorkerCompleted += 
    (sender, args) => CompleteSave();
  Model = new Model {
    Action = DataBindingFactory.Create<ToDoAction>(),
    AllowEditing = new Observable<bool>(true)
  };
}

在 BackgroundWorker 用于执行实际保存已拆分为两个不同的部分的过程。除了从该拆分 ’s 非常类似于我在编辑方案中处理的方式。需要特别注意的另一个有趣的位是 AllowEditing 属性 ; 此属性用于在窗体中锁定 UI,当执行保存操作。此方式您可以安全地使用该会话另一个线程中知道 won’t 有到该会话或任何通过该窗体及其实体的并发访问。

现在应该是在保存过程本身相当熟悉。let’s 首先查看 OnSave 方法:

public void OnSave() {
  Model.AllowEditing.Value = false;
  saveBackgroundWorker.RunWorkerAsync();
}

此方法负责禁用在表单中进行编辑,然后关闭后台进程 kicking。 在后台执行实际的保存。 该代码 shouldn’t 网站是一个惊喜:

private void PerformActualSave() {
  using(var tx = Session.BeginTransaction()) {
    Model.Action.CreatedAt = DateTime.Now;
    
    Session.Save(Model.Action);
    tx.Commit();
  }
}

完成数据库实际保存在 BackgroundWorker 将 UI 线程中执行 CompleteSave 过程一部分:

private void CompleteSave() {
  Model.AllowEditing.Value = true;
  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  View.Close();
}

重新启用该窗体、 发布通知 (导致也更新相关的屏幕) 更新操作并最后关闭该窗体。我假定启用 UI isn’t 严格必要,但包括为完成起见,我它存在。

使用这种技术您可以充分利用后台处理,但不违反会话实例上线程处理的合同。为总是线程处理是非常好的方法来创建更积极响应的应用程序,但多线程的编程不是一个任务轻轻接近,因此需小心使用,使用此技术。

处理并发

并发是复杂的标题上的时间,最佳和 isn’t 局限于单独线程处理。请考虑其中有两个用户同时编辑同一实体的情况。其中之一将首先,命中提交按钮将更改保存到数据库。但问题是,第二个用户命中保存时发生什么按钮?

这称为并发冲突和 NHibernate 具有这样的冲突检测的相当几种方法。ToDoAction 实体具有一个 < 版本 / > 告诉 NHibernate 它需要显式执行开放式并发检查的字段。该 NHibernate 提供了有关并发选项的完整讨论,请参阅我的博客张贴在 的 ayende.com/Blog/archive/2009/04/15/nhibernate-mapping-concurrency.aspx

实质上是,并发解决方案分为两大类:

  • 悲观并发控制要求您在数据库上持有锁,并保持事务的时间长时间处于打开状态。如我前面讨论,这不是桌面应用程序中的是个好主意。
  • 这意味着您可以关闭数据库连接期间用户已经的乐观并发控制 “ 思考时间 ”。允许检测冲突的几种策略在乐观端了大部分 NHibernate 所提供的选项。

悲观并发控制会招致大量的性能开销,因为它 ’s 通常不可以接受。这,反过来,意味着您应该优选乐观并发控制。开放式并发与您尝试将数据保存在正常情况下,但准备好处理这种情况,其中已由另一个用户更改数据。

NHibernate 将清单作为一个 StaleObjectStateException 的这种情况期间保存或提交流程。您应用程序应捕获该异常并进行相应的行为。通常,它意味着您需要对用户显示某些种类的一条消息解释该实体已由另一个用户编辑和用户需要重做他们所做的更改。偶尔,您需要执行等,提出要将信息合并,或允许用户决定要保留哪个版本的更复杂操作。

因为第一个选项 — — 显示一条消息,并让用户重做任何更改 — 是更常见我显示如何实现的与 NHibernate,然后讨论简要如何可以实现其他解决方案。

一个有趣马上面临的问题是在会话中引发异常意味着会话不再可用。在任何并发冲突中显示 NHibernate 为异常。您可能会执行其已引发异常之后的会话的唯一一件事就是在其上调用 Dispose ; 任何其他操作将导致未定义的行为。

我将返回到编辑屏幕和实现并发处理有作为示例。我会将执行下列在编辑屏幕中添加创建并发冲突按钮:

public void OnCreateConcurrencyConflict() {
  using(var session = SessionFactory.OpenSession())
  using(var tx = session.BeginTransaction()) {
    var anotherActionInstance = 
      session.Get<ToDoAction>(Model.Action.Id);
    anotherActionInstance.Title = 
      anotherActionInstance.Title + " -";
    tx.Commit();
  }
MessageBox.Show("Concurrency conflict created");
}

这将创建一个新的会话,并修改标题属性。 当我试图保存该表单中的实体因为不知道这些更改的窗体上的该会话时,这将触发并发冲突。 图 7 显示了如何我处理的。

我只需换行,将保存到数据库中 try catch 块中并由我检测到并发冲突的情况下通知用户处理陈旧的状态异常的代码。 我然后替换该会话。

图 7。 处理并发冲突

public void OnSave() {
  bool successfulSave;
  try {
    using (var tx = Session.BeginTransaction()) {
      Session.Update(Model.Action);

      tx.Commit();
    }
    successfulSave = true;
  }
  catch (StaleObjectStateException) {
    successfulSave = false;
    MessageBox.Show(
      @"Another user already edited the action before you had a chance to do so. The application will now reload the new data from the database, please retry your changes and save again.");

    ReplaceSessionAfterError();
  }

  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  if (successfulSave)
    View.Close();
}

请注意我是 总是 调用 ActionUpdated,即使已收到并发冲突。 在此处 ’s 原因:即使我收到并发冲突,可能在应用程序的其余部分 doesn’t 知道关于它,和实体已在数据库中更改,所以我可能还提供应用程序的其余部分有机会向用户显示新的值。

最后,如果我被成功保存到数据库中我只关闭该窗体。 到目前为止,有得,’s 执行任何操作,但有是会话和实体替换仍需要考虑 (请参阅 的 图 8)。

图 8 更新会话和实体

protected void ReplaceSessionAfterError() {
  if(session!=null) {
    session.Dispose();
    session = sessionFactory.OpenSession();
    ReplaceEntitiesLoadedByFaultedSession();
  }
  if(statelessSession!=null) {
    statelessSession.Dispose();
    statelessSession = sessionFactory.OpenStatelessSession();
  }
}

protected override void 
  ReplaceEntitiesLoadedByFaultedSession() {
  Initialize(Model.Action.Id);
}

您可以看到我替换该会话或无状态的会话通过处置它们并打开新的。在一个会话的情况下我还要求对演示者,以替换已加载由错误会话的所有实体。NHibernate 实体紧密关联到他们的会话,当该会话变得不可用的 ’s 替换实体也通常最佳。它不是 必需 — 实体 aren’t 打算突然停止工作 — 但等惰性加载将不再起作用。而是,我支付替换实体比试图我可以或不能遍历对象图,在某些情况下,计算出的成本。

实体替换的实现是只在这种情况下调用初始化方法来完成的。这是我介绍了在编辑窗体的情况下在同一个初始化方法。此方法只是从数据库获取该实体,并将其设置成模型属性 — — 任何令人兴奋的内容。在更复杂的方案中它可能会替换在单个窗体中使用的几个实体实例。

该问题的相同方法持有不仅并发冲突而且可能获得 NHibernate 已经会话中的任何错误。一旦得到错误,您必须替换该会话。并且替换该会话时, 您可能应该重新加载在新形状使用旧的会话,只是为了能在安全的端加载任何实体。

冲突管理

我希望本文中时接触最后一个主题是更复杂的并发冲突管理技术。基本上有 ’s 只有一个选项:允许用户进行数据库中的版本和用户只需修改的版本之间决策。

图 9 显示合并屏幕模型。正如您看到此处您只需显示用户这两个选项,并询问要选择哪一个它们将接受。任何并发冲突解决方案以某种方式基于此想法。您可能希望以不同的方式呈现但的 ’s 原理,您可以从此处外推。

图 9 的 用于管理更改冲突的 UI

在编辑屏幕中更改冲突解决为:

catch (StaleObjectStateException) {
  var mergeResult = 
    Presenters.ShowDialog<MergeResult?>(
    "Merge", Model.Action);
  successfulSave = mergeResult != null;

  ReplaceSessionAfterError();
}

显示合并对话框,如果用户使有关合并决策我决定它已成功保存 (这将关闭编辑窗体)。 请注意,我向合并对话框传递当前编辑的操作所以它知道实体的当前状态。

合并对话框演示者十分简单:

public void Initialize(ToDoAction userVersion) {
  using(var tx = Session.BeginTransaction()) {
    Model = new Model {
      UserVersion = userVersion,
      DatabaseVersion = 
        Session.Get<ToDoAction>(userVersion.Id),
        AllowEditing = new Observable<bool>(false)
    };  

    tx.Commit();
  }
}

在启动时,我从数据库获取当前的版本,并显示它并在用户更改的版本。 如果用户接受数据库版本,我 don’t 有很多做,因此我只需关闭该窗体:

public void OnAcceptDatabaseVersion() {
  // nothing to do
  Result = MergeResult.AcceptDatabaseVersion;
  View.Close();
}

如果用户想要强制他们自己的版本,它只 ’s 稍微复杂一些:

public void OnForceUserVersion() {
  using(var tx = Session.BeginTransaction()) {
    //updating the object version to the current one
    Model.UserVersion.Version = 
      Model.DatabaseVersion.Version;
    Session.Merge(Model.UserVersion);
    tx.Commit();
  }
  Result = MergeResult.ForceDatabaseVersion; 
  View.Close();
}

用户已经版本中接受所有永久的值,并将它们复制到当前会话内的实体实例使用 NHibernate 已经合并功能。在起的方式作用该合并强制数据库值顶部的用户值在两个实例。

这是实际安全甚至与在其他会话死和不见了,因为合并方法协定可以确保它 doesn’t 尝试遍历惰性加载的关联。

请注意: 我尝试在合并之前,我设置用户已经版本属性为数据库已经版本属性。这是因为在这种情况下需要显式地覆盖该版本。

此代码 doesn’t 尝试处理递归并发冲突 (即,获取并发冲突解决第一个的结果)。为您的左作为一个练习。

尽管本文所述的挑战数构建桌面应用程序 NHibernate isn’t 任何更难比生成 NHibernate Web 应用程序。然后,在这两种方案中我相信使用 NHibernate 将使您的生活更容易、 更可靠应用程序和总体系统更方便地更改和处理。

Oren Eini (谁工作笔名 Ayende Rahien) 是几个打开的源项目 (NHibernate 和在它们之间的 Castle) 的活动成员,并且是 (Rhino Mocks、 NHibernate 查询分析器并且在它们之间的 Rhino Commons) 许多其他的 founderEini 程序还负责 NHibernate 事件探查器 ( nhprof.com ),NHibernate 的可视调试器。您可以按照在 ayende.com/Blog Eini 已经工作。

感谢到下面的技术专家为审阅此文章:Howard Dierking