65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 示例 – 第十一章 – NHibernate 实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (15投票s)

2010年11月2日

CPOL

11分钟阅读

viewsIcon

93675

在 NHibernate 中使用存储库的单元工作模式。

Previous Next
第 X 章 第 XII 章

系列文章

WCF 示例是一个系列文章,它描述了如何使用 WCF 进行通信和 NHibernate 进行持久化,来设计和开发 WPF 客户端。系列文章的 介绍 描述了文章的范围,并从高层次讨论了架构解决方案。该系列的源代码可在 CodePlex 上找到。

章节概述

在第 X 章中,我们讨论了依赖注入如何通过提供一个简单的机制来更改我们的后端实现来优化我们的设计;我们还演示了如何配置应用程序以使用内存持久化实现。使用内存实现进行工作是非常高效的,它有助于我们专注于对象建模,并将集成应用程序到后端数据库的繁重而昂贵的工作推迟到后期阶段,那时模型可能更加稳定。这种方法可以分为两个主要阶段(在 UAT 之前):对象建模和集成(到后端数据库)。然而,如果可行,在早期阶段有人关注 NHibernate 的实现也有一些好处,有时可能会发现模型更改在实体持久化到数据库时是必需的。在这个过程中,一套可用于这两种实现并能成功执行的良好测试是确保顺畅过渡的关键。

在本章中,我们将把 NHibernate 实现引入 eDirectory 解决方案。尽管需要一些步骤,但我们期望展示新组件如何轻松地集成到我们现有的解决方案中,以及从客户端应用程序的角度来看,从内存模式切换到 NHibernate 模式是多么透明。在下面的图表中,我们可以观察到与内存模式唯一的区别在于所使用的 TransFactory 实现是一个新的 NHibernate 组件。值得注意的是,我们仍然在一个进程中执行应用程序。

设置后端数据库

在继续之前,我们需要为 eDirectory 解决方案配置一个数据库实例。在此示例中,我们使用 SQL Server,但您也可以尝试使用 Oracle、MySQL,甚至 SQLite 数据库。我们假设您已安装 SQL Express,但您会发现本章提供的 NHibernate 配置文件在 SQL Server 2005/2008 服务器实例上运行良好。

您需要创建一个名为 eDirectory 的数据库,您可以在 Visual Studio 中按照以下步骤创建它:

  1. 在服务器资源管理器中,选择“创建新数据库”选项。
  2. 在我们的示例中,本地实例名为 SQLEXPRESS,我们使用 Windows 身份验证,并将数据库命名为 eDirectory。

客户配置映射文件

对于每个实体(我们解决方案中的 Customer 类),我们需要定义 NHibernate 类和数据库后端之间的映射。在 eDirectory 解决方案中,我们将使用 XML 配置文件,但我们也可以使用 NFluent 声明。

我们在 Domain 项目中添加了一个 Mappings 文件夹,然后添加了 Customer.hbm.xml。这是一个 NHibernate 配置文件,您可能希望指定 NHibernate 架构文件定义,以便可以使用 intellisense。

XML 文件的一个小细节是,它需要设置为嵌入的资源,以便 NHibernate 配置助手可以在运行时在程序集中找到它。

首先,我们声明包含 Customer 类程序集和命名空间的标题部分。

在正文部分,我们声明 Customer 类的映射。

值得注意的是 Id 属性是如何声明的,我们指出该字段使用了 SQL 标识字段,并且如果类属性的值为零,则表示 NHibernate 实例尚未持久化。其他三个字段则不言而喻。

eDirectory.NHibernate 项目

我们需要一个新的项目来进行 NHibernate 实现,我们将在此项目中声明五个新类。首先,除了添加对 eDirectory.Common 和 eDirectory.Domain 的引用外,我们还需要对 NHibernate 和 NHibernate.Linq 程序集进行引用。

我们需要实现与内存版 eDirectory.Naive 程序集中相同的类。

NHibernate 的存储库实现比内存版的要简单,这可能会让一些人感到惊讶。

namespace eDirectory.NHibernate.Repository
{
    public class RepositoryNh<TEntity>
         : IRepository<TEntity>
    {

        private readonly ISession SessionInstance;

        public RepositoryNh(ISession session)
        {
            SessionInstance = session;
        }

        #region Implementation of IRepository<TEntity>

        public TEntity Save(TEntity instance)
        {
            SessionInstance.Save(instance);
            return instance;
        }

        public void Update(TEntity instance)
        {
            SessionInstance.Update(instance);
        }

        public void Remove(TEntity instance)
        {
            SessionInstance.Delete(instance);
        }

        public TEntity GetById(long id)
        {
            return SessionInstance.Get<TEntity>(id);
        }

        public IQueryable<TEntity> FindAll()
        {
            return SessionInstance.Linq<TEntity>();
        }

        #endregion
    }
}

上述实现中最关键的两个方面是 Session 如何解决我们所有的实现方法,以及在 FindAll 方法中使用通用的 Linq 扩展方法。这段简短的代码为执行查询提供了极大的灵活性,而无需创建专门的存储库方法。这真是个亮点。

然后我们有了 RepositoryLocator 的实现;这个类的目的是管理存储库,并帮助查找给定实体的存储库实例。我们所做的是将我们在内存实现中定义的一些可以与 NHibernate 版本重用的功能推到基类。这样,新类就相当简单了。

namespace eDirectory.NHibernate.Repository
{
    public class RepositoryLocatorNh
        :RepositoryLocatorBase
    {
        private readonly ISession SessionInstance;

        public RepositoryLocatorNh(ISession session)
        {
            SessionInstance = session;
        }
        
        #region Overrides of RepositoryLocatorBase

        protected override IRepository<T> CreateRepository<T>()
        {
            return new RepositoryNh<T>(SessionInstance);
        }

        #endregion
    }
}

TransManager 实现稍微复杂一些,但也许最重要的一点是创建管理器新实例时需要传递的 Session 实例。这个实现管理事务,不像内存实现。

namespace eDirectory.NHibernate.TransManager
{
    public class TransManagerNh
        :TransManagerBase
    {
        private readonly ISession SessionInstance;

        public TransManagerNh(ISession session)
        {
            SessionInstance = session;
            Locator = new RepositoryLocatorNh(SessionInstance);
        }

        #region Overriden Base Methods

        public override void BeginTransaction()
        {
            base.BeginTransaction();
            if (SessionInstance.Transaction.IsActive) return;
            SessionInstance.BeginTransaction();
        }

        public override void CommitTransaction()
        {
            base.CommitTransaction();
            if (!SessionInstance.Transaction.IsActive) return;
            SessionInstance.Transaction.Commit();
        }

        public override void Rollback()
        {
            base.Rollback();
            if (!SessionInstance.Transaction.IsActive) return;
            SessionInstance.Transaction.Rollback();
        }

        protected override void Dispose(bool disposing)
        {
            if (!disposing) return;
            // free managed resources
            if (!IsDisposed && IsInTranx)
            {
                Rollback();
            }
            Close();
            Locator = null;
            IsDisposed = true;
        }
        
        private void Close()
        {
            if (SessionInstance == null) return;
            if (!SessionInstance.IsOpen) return;
            SessionInstance.Close();
        }

        #endregion
    }
}

最后一个实现是 Factory,但在我们介绍它之前,我们需要介绍一个管理 NHibernate 基础设施方面的类,一个是配置设置,另一个是架构导出助手类。NhBootStrapper 类有两个属性。

namespace eDirectory.NHibernate.BootStrapper
{
    public class NhBootStrapper
    {
        private Configuration NhConfigurationInstance;

        public Configuration NhConfiguration
        {
            get
            {
                if (string.IsNullOrEmpty(ConfigurationFileName))
                    throw new ArgumentException(
                      "ConfigurationFileName property was blank");
                NhConfigurationInstance = new Configuration();
                NhConfigurationInstance.Configure(ConfigurationFileName);
                NhConfigurationInstance.AddAssembly(typeof(Customer).Assembly);
                return NhConfigurationInstance;
            }
        }


        private SchemaExport eDirectorySchemaExportInstance;

        public SchemaExport eDirectorySchemaExport
        {
            get
            {
                if (eDirectorySchemaExportInstance != null)
                    return eDirectorySchemaExportInstance;
                eDirectorySchemaExportInstance = new SchemaExport(NhConfiguration);
                return eDirectorySchemaExportInstance;
            }
        }


        public string ConfigurationFileName { get; set; }
    }
}

在调用 NhConfiguration 之前,我们需要设置 ConfigurationFileName 属性。首次调用此属性时,将从给定文件创建 NHibernate 配置类实例,并且还将声明 eDirectory.Domain 程序集中声明的映射。

eDirectorySchemaExport 不打算由 eDirectory 生产应用程序使用,它用于通过辅助应用程序(如测试或负责创建数据库架构的控制台应用程序)重新生成 eDirectory 数据库架构。

工厂类的实现如下:

namespace eDirectory.NHibernate.TransManager
{
    public class TransManagerFactoryNh
        : ITransFactory
    {

        private ISessionFactory SessionFactoryInstance;

        private ISessionFactory SessionFactory
        {
            get
            {
                if (SessionFactoryInstance != null)
                    return SessionFactoryInstance;
                SessionFactoryInstance = InitializeSessionFactory();
                return SessionFactoryInstance;
            }
        }

        #region Implementation of ITransFactory

        public ITransManager CreateManager()
        {
            return new TransManagerNh(SessionFactory.OpenSession());
        }

        #endregion

        #region Properties

        public string ConfigurationFileName { get; set; }

        #endregion

        #region Session Factory Initializer

        private ISessionFactory InitializeSessionFactory()
        {
            var nhConfiguration = new NhBootStrapper 
                  {ConfigurationFileName = this.ConfigurationFileName};
            return nhConfiguration.NhConfiguration.BuildSessionFactory();
        }

        #endregion
    }
}

CreateManager 方法负责将 Session 实例传递给事务管理器,事务管理器使用该实例创建 RepositoryLocator 的实例。Session 实例是从 NHibernate SessionFactory 生成的,该 SessionFactory 是使用前面提到的 NhBootStrapper 类创建的。ConfigurationFileName 属性是可用的,因此可以通过依赖注入进行“注入”;稍后我们将看到客户端如何使用它。

客户端更改

这是 NHibernate 实现中最有趣的一个方面。让客户端使用 NHibernate 并将数据持久化到数据库的唯一需要的是一个新的 Spring.Net 配置文件。甚至不需要更改一行代码。这不好吗?

新配置文件名为 NhConfiguration.xml;如果我们将其与内存版本进行比较,唯一的改动在于 TransFactory 引用的定义。

我们也在这个部分声明了配置文件名。

<!-- Transaction Factory -->
<object
    id="TransFactoryRef"
    type="eDirectory.NHibernate.TransManager.TransManagerFactoryNh, eDirectory.NHibernate">
  
  <property name="ConfigurationFileName" value="nhibernate.cfg.xml" />
</object>

NHibernate 配置文件

我们只介绍配置文件的基本方面;您可能需要查阅参考文档以全面了解每个配置设置。

<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
    <session-factory name="NHibernate.Test">
        <property name="connection.provider">
          NHibernate.Connection.DriverConnectionProvider
        </property>
        <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
        <property name="connection.driver_class">
          NHibernate.Driver.SqlClientDriver</property>
        <property name="connection.connection_string">
          Server=.\SQLEXPRESS;Database=eDirectory;Integrated Security=SSPI;
        </property>

        <property name="show_sql">false</property>
        <property name="proxyfactory.factory_class">
          NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
        </property>
        <property name="hbm2ddl.keywords">none</property>
        <property name="cache.use_second_level_cache">true</property>
        <property name="cache.use_query_cache" >true</property>
        <property name="cache.provider_class">
          NHibernate.Cache.HashtableCacheProvider</property>
    </session-factory>
</hibernate-configuration>

如果您的 SQL Server 实例名称与默认的 SQL Server Express 名称不同,您可能需要更改连接字符串;此外,您可能希望启用 show_sql 以查看 SQL 语句;不幸的是,这只在运行测试或控制台应用程序时有效。

控制台应用程序 - 生成架构

此时,我们几乎准备好运行客户端了。我们在解决方案中创建了一个新的配置设置,以确保正确的程序集被部署到客户端文件夹。

我们还更改了客户端的 app.config,使其现在使用 NHibernate 配置。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="SpringConfigFile" value="file://NhConfiguration.xml" />
  </appSettings>
</configuration>

但是,如果我们此时尝试执行应用程序,将会抛出以下异常:

如果我们检查内部异常,我们会发现以下描述:{"无效的对象名称 'Customer'"},这意味着 Customer 表不存在。我们需要某种机制来生成我们的架构;我们可以手动完成,但幸运的是,NHibernate 能够根据我们的映射定义生成数据库架构,这在从头开始创建数据库的新项目中被证明是一种好方法。

在 eDirectory 中,我们提供了一个生成架构的控制台应用程序;它称为 eDirectory.Console。这目前是一个非常基础的应用程序,它只有一个方法和几行代码。此外,它为我们做了很多魔法。

class Program
{
    static void Main(string[] args)
    {
        var nhBootStrapper = new NhBootStrapper
                                 {
                                     ConfigurationFileName = @"nhibernate.cfg.xml"
                                 };

        var connString = nhBootStrapper.NhConfiguration.GetProperty(
                            "connection.connection_string");
        System.Console.WriteLine("Updating database schema for: " + connString);
        nhBootStrapper.eDirectorySchemaExport.Create(true, true);
    }
}

控制台应用程序可以轻松地转换为另一种类型的客户端,与 WPF 或测试项目并行。例如,在最近的一个项目中,我们使用这种方法,让一个控制台应用程序负责使用实体及其持久化功能将数据从旧数据库迁移到新数据库。

如果执行控制台应用程序,将调用以下 SQL 语句:

-- Updating database schema for: 
--       Server=.\SQLEXPRESS;Database=eDirectory;Integrated Security=SSPI;

if exists (select * from dbo.sysobjects where id = object_id(N'Customer') 
   and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table Customer

create table Customer (
   Id BIGINT IDENTITY NOT NULL,
   FirstName NVARCHAR(50) not null,
   LastName NVARCHAR(50) not null,
   Telephone NVARCHAR(20) not null,
   primary key (Id)
)

不算太坏。让我们再次尝试执行我们的客户端,在 NHibernate 配置文件中启用 show_sql 设置。如果我们创建一个名为“Joe Bloggs”的记录,我们可以观察到以下语句在我们新数据库中执行:

NHibernate: INSERT INTO Customer (FirstName, LastName, Telephone) 
  VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY();@p0 = 'Joe', 
  @p1 = 'Bloggs', @p2 = '9999-8888'
NHibernate: SELECT this_.Id as Id0_0_, this_.FirstName as FirstName0_0_, 
  this_.LastName as LastName0_0_, 
  this_.Telephone as Telephone0_0_ FROM Customer this_

您可能想打开数据库检查记录是否已成功创建。

测试重构

到目前为止,我们已经创建了一些使用内存模式的测试,在许多项目中这已经足够了;尽管在一些项目中,也会创建端到端测试以确保使用数据库时的功能正确性。但在许多方面,针对数据库调用测试可能很昂贵。在我们的 eDirectory 解决方案中,我们可以演示如何让同一个测试在内存模式和 NHibernate 模式下运行。

处理数据库和测试时,有几个方面需要更多的努力。第一个是在测试开始之前重新创建数据。第二个是在下一个测试执行之前清理所有更改。这个问题有不同的解决方案,但在 eDirectory 应用程序中,我们将提出一个简单的解决方案:每个测试将从头开始重新创建整个数据库。有些人可能会认为这是解决我们问题的糟糕方案,但它很简单并且效果很好;它可能只是性能不太好。

我们提出的是一种开发方法,开发人员在内存模式下创建和执行测试,然后一个持续集成服务器可以负责在数据库上执行相同的测试。

为了让测试项目与 NHibernate 一起工作,我们需要添加一些东西。首先,我们需要添加一些引用,以及添加到客户端项目中的相同配置文件。我们还需要对新的 eDirectory.NHibernate 项目进行引用。然后我们需要修改 app.config 来指示我们正在使用 NHibernate 配置。对于像我这样使用 ReSharper 的人来说,可能需要做一些额外的工作来确保配置文件被部署到 MS 测试执行的正确位置。

关键是确保测试不会因为我们在内存模式或 NHibernate 模式下运行而改变;eDirectoryTestBase 基类可以提供我们所需要的一切,我们只需要添加一个新方法来确保在每个测试开始时都创建数据库。

namespace eDirectory.UnitTests
{
    [TestClass]
    [DeploymentItem("nhibernate.cfg.xml")]
    public abstract class eDirectoryTestBase
    {
        [TestInitialize]
        public virtual void TestsInitialize()
        {
            InitialiseDatabase();
        }

        private static void InitialiseDatabase()
        {
            var nHFactory = 
              GlobalContext.Instance().TransFactory as TransManagerFactoryNh;
            if (nHFactory == null) return;
            var nhBootStrapper = 
                new NhBootStrapper {ConfigurationFileName = 
                nHFactory.ConfigurationFileName};
            nhBootStrapper.eDirectorySchemaExport.Create(false, true);
        }

        [TestCleanup]
        public virtual void TestCleanUp()
        {
            ResetLocator();
        }

        private static void ResetLocator()
        {            
            ...
        }
    }
}

InitialiseDatabase 检查测试是否在 NHibernate 模式下执行;如果是,则使用 eDirectorySchemaExport 在测试开始时重新生成数据库架构。可以看到,测试花费的时间稍微长一些,但让它们在数据库上执行是值得的。

如果我们启用了 show_sql 选项,我们可以检索每个测试执行的 SQL 语句;例如,当执行 UpdateCustomer 测试时,会执行以下语句:

CustomerServiceTests.UpdateCustomer : Passed

NHibernate: INSERT INTO Customer (FirstName, LastName, Telephone) 
  VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY();@p0 = 'Joe', 
  @p1 = 'Bloggs', @p2 = '9999-8888'
NHibernate: SELECT customer0_.Id as Id1_0_, customer0_.FirstName as FirstName1_0_, 
  customer0_.LastName as LastName1_0_, customer0_.Telephone 
  as Telephone1_0_ FROM Customer customer0_ WHERE customer0_.Id=@p0;@p0 = 1
NHibernate: UPDATE Customer SET FirstName = @p0, LastName = @p1, 
  Telephone = @p2 WHERE Id = @p3;@p0 = 'Joe', @p1 = 'Bloggs', 
  @p2 = '8888-8888', @p3 = 1

章节总结

在本章中,我们演示了如何使用上一章介绍的依赖注入功能轻松地将 NHibernate 集成到 eDirectory 解决方案中。为了让应用程序使用数据库工作,我们没有更改客户端的一行代码。我们还解释了如何利用 NHibernate 功能从我们的域映射创建架构,并讨论了如何增强我们的内存测试,使其可以在无需更改的情况下针对数据库执行。

我们可能没有涵盖继承、父子关系或多对多关系的任何复杂持久化场景;但我们希望我们已经展示了使用 NHibernate 将解决方案组件集成到数据库相对容易。RepositoryRepositoryLocatorTransactionManagerFactory 类可以完美地用于一个全面的企业解决方案,只需很少或无需更改。

在下一章中,我们将讨论 WCF 实现,并第一次看到我们的应用程序如何在客户端和服务器两个实例之间解耦。

© . All rights reserved.