Setting up session per presenter
session-per-presenter模式创建会话
在桌面应用程序中使用MVP(model-view-presenter)模式, 最好每个presenter使用一个会话. 这种方法也适用于model-view-view-model模式.有关这些模式的更多信息请参考http://en.wikipedia.org/wiki/Model-view-presenter.
本节介绍如何使用依赖注入来实现session-per-presenter模式.
准备
你将需要下载Ninject,下载地址http://github.com/remogloor/ninject.extensions.namedscope.
下载源代码的ZIP包并解压. 打开Ninject.Extensions.NamedScope.sln解决方案并编译.将生成的Ninject.dll和Ninject.Extensions.NamedScope.dll复制到 我们的Cookbook解决方案下的Lib文件夹中.
如果你对依赖注入的概念还不是很熟悉,可以先看下TekPub上的一个免费视频,地址:http://tekpub.com/view/concepts/1 .
提示
使用其他的依赖注入框架也可以完成本节的示例. 只需用你中意的DI框架的等价配置去替代NinjectBindings类.
步骤
1. 在解决方案中新添一个名为SessionPerPresenter的控制台项目.
2. 添加引用:Eg.Core项目(第一章中), NHibernate.dll, NHibernate.ByteCode.Castle.dll, log4net.dll, Ninject.dll, 和 Ninject.Extensions.NamedScope.dll.
3. 添加一个App.config文件并在文件中创建NHibernate和log4net的配置节点.可以参考Configuring NHibernate和Configuring NHibernate logging小节示例.
4. 为该项目添加一个名为Data的文件夹.
5. 在Data文件夹中创建一个IDao<TEntity>接口,代码如下:
public interface IDao<TEntity> : IDisposable where TEntity : class { IEnumerable<TEntity> GetAll(); }
6. 创建一个该接口的实现,代码如下:
public class Dao<TEntity> : IDao<TEntity> where TEntity : class { private readonly ISessionProvider _sessionProvider; public Dao(ISessionProvider sessionProvider) { _sessionProvider = sessionProvider; } public void Dispose() { _sessionProvider.Dispose(); } public IEnumerable<TEntity> GetAll() { var session = _sessionProvider.GetCurrentSession(); IEnumerable<TEntity> results; using (var tx = session.BeginTransaction()) { results = session.QueryOver<TEntity>() .List<TEntity>(); tx.Commit(); } return results; } }
7. 在Data文件夹中创建一个ISessionProvider接口,代码如下:
public interface ISessionProvider : IDisposable { ISession GetCurrentSession(); void DisposeCurrentSession(); }
8. 创建该接口的实现,代码如下:
public class SessionProvider : ISessionProvider { private readonly ISessionFactory _sessionFactory; private ISession _currentSession; public SessionProvider(ISessionFactory sessionFactory) { Console.WriteLine("Building session provider"); _sessionFactory = sessionFactory; } public ISession GetCurrentSession() { if (null == _currentSession) _currentSession = _sessionFactory.OpenSession(); return _currentSession; } public void DisposeCurrentSession() { _currentSession.Dispose(); _currentSession = null; } public void Dispose() { if (_currentSession != null) _currentSession.Dispose(); _currentSession = null; } }
9. 创建一个名为NinjectBindings的Ninject模块,代码如下:
public class NinjectBindings : NinjectModule { public override void Load() { const string presenterScope = "PresenterScope"; var asm = GetType().Assembly; var presenters = from t in asm.GetTypes() where typeof (IPresenter).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract select t; foreach (var presenterType in presenters) Kernel.Bind(presenterType) .ToSelf() .DefinesNamedScope(presenterScope); Kernel.Bind<ISessionProvider>() .To<SessionProvider>() .InNamedScope(presenterScope); Kernel.Bind(typeof(IDao<>)) .To(typeof(Dao<>)); } }
10. 在项目的根目录下,创建一个名为ProductListView的类,代码如下:
public class ProductListView { private readonly string _description; private readonly IEnumerable<Product> _products; public ProductListView( string description, IEnumerable<Product> products) { _description = description; _products = products; } public void Show() { Console.WriteLine(_description); foreach (var p in _products) Console.WriteLine(" * {0}", p.Name); } }
11. 创建一个继承自IDisposable的公共的IPresenter接口. 这个接口先空着.
12. 创建一个名为MediaPresenter的类,代码如下:
public class MediaPresenter : IPresenter { private readonly IDao<Movie> _movieDao; private readonly IDao<Book> _bookDao; public MediaPresenter(IDao<Movie> movieDao, IDao<Book> bookDao) { _movieDao = movieDao; _bookDao = bookDao; } public ProductListView ShowBooks() { return new ProductListView("All Books", _bookDao.GetAll().OfType<Product>()); } public ProductListView ShowMovies() { return new ProductListView("All Movies", _movieDao.GetAll().OfType<Product>()); } public void Dispose() { _movieDao.Dispose(); _bookDao.Dispose(); } }
13. 创建一个名为ProductPresenter的类,代码如下:
public class ProductPresenter : IPresenter { private readonly IDao<Product> _productDao; public ProductPresenter(IDao<Product> productDao) { _productDao = productDao; } public ProductListView ShowAllProducts() { return new ProductListView("All Products", _productDao.GetAll()); } public virtual void Dispose() { _productDao.Dispose(); } }
14. 在Program.cs的Main方法中,添加下述代码:
var nhConfig = new Configuration().Configure(); var sessionFactory = nhConfig.BuildSessionFactory(); var kernel = new StandardKernel(); kernel.Load(new Data.NinjectBindings()); kernel.Bind<ISessionFactory>() .ToConstant(sessionFactory); var media1 = kernel.Get<MediaPresenter>(); var media2 = kernel.Get<MediaPresenter>(); media1.ShowBooks().Show(); media2.ShowMovies().Show(); media1.Dispose(); media2.Dispose(); using (var product = kernel.Get<ProductPresenter>()) { product.ShowAllProducts().Show(); } Console.WriteLine("Press any key"); Console.ReadKey();
15. 如果你愿意的话,可以在NHCookbook数据库中创建一些测试数据:product, book和movie数据
16. 编译运行,结果如下图所示:
原理
这个示例中有几处有意思的地方. 首先,我们创建了一个稍微复杂的对象图. MediaPresenter的每个实例如下图所示:
上图中,两个数据访问对象共享一个会话实例provider.这通过依赖注入框架-Ninject的配置来实现
在NinjectBindings中, 我们通过匹配我们的服务接口来实现他们的匹配. 将泛型接口IDao<>绑定到Dao<>,因此对IDao<Book>的请求将被解析为对Dao<Book>的请求, IDao<Movie> 和 Dao<Movie>等也是同样的原理.
通过使用DefinesNamedScope和InNamedScope我们完成了一个session per presenter模式. 我们在程序集中查找所有的IPresenter实现.每个presenter都被绑定并且定义了PresenterScope. 当将ISessionProvider绑定到SessionProviderImpl时,我们使用InNamedScope("PresenterScope")来指明每个presenter只有一个会话provider.
Kernel.Get<MediaPresenter>()的调用将返回一个新的presenter实例(该实例被完全构造出来以备使用).它将有两个数据访问对象共享一个共同的会话provider . 不在使用该presenter时,要关闭会话和释放数据库连接,此时请确保调用了Dispose().
在Dao中一个标准的Save方法,其代码如下所示:
var session = _sessionProvider.GetCurrentSession(); try { session.SaveOrUpdate(entity); } catch (StaleObjectStateException) { _sessionProvider.DisposeCurrentSession(); throw; }
请注意在catch块中我们是如何立刻丢弃会话的.当NHibernate从一个内部会话调用中抛出一个异常时,该会话的状态是未定义的.在该会话上您唯一可做的安全操作只剩下:Dispose(). 这使得我们可以轻易的应对任何异常,因为崩溃掉的会话将被丢弃,所以一个新的会话会将其替代.
应当注意到失败的会话和实体仍然是相关联的. 应该将这些实体附加到一个新的会话,因为对失败会话进行的任何操作,包括延迟加载在内都可能引起更多的异常. 本章后面的session.Merge小节会讨论实现附加的一个方法.
扩展
与web应用程序相比,在桌面应用程序中边界不太好界定,但是有两种很常见的反模式来处理NHibernate会话.第一个, 单例会话有下述问题:
- 没有定义将会话flush到数据库的点.
- 应用程序中不相关的部分之间存在不可测试的交互.
- 无法从StaleObjectExceptions或其他引起会话崩溃的异常中轻易恢复.
- 一个带状态的单例模式通常是一个不好的架构.
第二个, 一个微型会话,这样的会话会被打开,执行一个简单的操作后马上关闭.这样的情况下unit of work毫无优势可言,最适合该情况的应该是使用会话缓存(尤其适合从数据库频繁重复抓取实体的情况.).