NHibernate实践总结(二) 在mapping文件中设置抓取策略对HQL与Criteria造成不同影响的测试与验证(转http://www.cnblogs.com/dddd218/archive/2009/09/03/1558999.html)

Coordinator
Oct 25, 2011 at 3:52 PM
一、引
  
  在上一篇文章NHibernate实践总结(一)中,主要罗列了自己的一部分实践总结,其中包括:
(1)在NHibernate的mapping文件中,所有关联实体和关联集合最好都保持默认的延迟加载,即lazy="true",以避免关联数据不必要的过量加载。
(2)若确实需要获取关联数据,则可编写join或dynamic fetching join的HQL(或Criteria)查询加载关联数据。
(3)尽量避免在mapping文件中进行这些fetching strategy设置(暂且称为抓取策略设置):如fetch="join"(关联抓取),fetch="subselect"(子查询抓取),batch-size="20"(批量抓取)等。因为在 mapping文件中的设置是global性质的设置,会对整个系统产生全局性的影响,更重要的是,有些设置对Get、HQL和Criteria都产生影响,而有些设置对Get、Criteria有影响却对HQL不影响。这些微妙的差异容易导致在系统中引入诡秘的bug,同时还会增加开发的难度,以及维护的负担。为了解决这个问题,最简单有效的办法就是避免在mapping文件中进行这些设置。
  今天这篇文章,主要介绍上面(3)中提到的:HQL与Criteria对mapping文件中设置的抓取策略的差异现象,即mapping文件中的fetch="join"抓取策略设置,对Get、Criteria有影响对HQL不影响的现象。当一开始遇到这个现象时,相信园子里的不少朋友会和我一样,还以为是自己的代码写错了或是mapping文件设置错了呢,经过调试确认代码和设置都没错之后,甚感惊讶,就怀疑是NHibernate的Bug。在网上搜索相关资料 后才知道Hibernate(NHibernate)存在此现象,My God,我又没有先知先觉,谁想得到呢?气得真想把电脑给砸了,算是被NHibernate耍了几个小时的调试时间,从此以后在实际项目中就再也没在mapping文件中设置过任何fetching strategy。
  同样的设置,对HQL和Criteria造成不同的表现,我个人认为不是一个good idea。那么Hibernate(NHibernate)为什么存在此现象,肯定有其道理,由于时间关系我没有深究,有兴趣的朋友可以去查查相关资料。

二、测试与验证过程 
  
  接下来通过编写、测试一个简单的实例来验证这种差异,并在文章最后提供该例子的下载(该例子仅用于验证本文观点,并不表示任何实际项目中的规范代码)
1、开发环境
  先介绍一下这个例子的开发环境:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->NHibernate 2.0
Database:SQL Server 2005 (根据你的实际情况附加数据库,或者创建数据库数据表和测试数据,并修改NHibernate配置文件中的数据数据库链接)
IDE:Visual Studio 2008 English version
测试工具:NUnit2.4.8
2、场景
  有部门(Department)和员工(Employee)两个实体,假定两者之间存在一对多的关系,即一个部门拥有多个员工,某个员工只属于一个部门。通过在Departmentmapping文件中设置fetch="join"(关联抓取),使得在查找Department数据的同时获取关联集合数据Employee。
3、目标
  验证fetch="join"设置对Get、Criteria查询有效,对HQL查询无效。
4、实例项目截图




5、详细说
(1)测试数据库与测试数据
  Database目录下面是测试用的数据库文件NHibernatePractice2.mdf,包含有两张数据表Department与Employee,其中Employee表的DepartmentId是外键,引用Department的主键。表中测试数据如下:
数据表及测试数据
  为了测试方便,你可以直接附加数据库文件NHibernatePractice2.mdf到SQL Server2005,否则也可以自己创建数据库,然后手工(或使用NHibernate的SchemaExport)创建数据表、插入测试数据。
(2) 引用的类库Lib
  Lib目录下面是引用的dll,包括:NHibernate.dll、nunit.framework.dll、Castle.DynamicProxy2.dll、Iesi.Collections.dll、log4net.dll。为项目添加这些引用。
(3)hibernate.cfg.xml
  创建NHibernate配置文件,注意该文件属性设置为“Copy always”:配置如下:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--><?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  
<session-factory>
    
    
<!--根据你自己的情况修改数据库驱动、数据库链接字符-->
    
<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=(local);Initial Catalog=NHibernatePractice2;Integrated Security=SSPI</property>
    
<property name="show_sql">true</property>
    
<mapping assembly="NHibernatePractice2"/>
    
  
</session-factory>
</hibernate-configuration>
(4)NHibernateHelper.cs
  接下来创建NHibernate辅助类,用于获取NHibernate的session,其中应用了Singleton模式:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->namespace NHibernatePractice2
{
    
//辅助类,用于获取Session
    
//应用了Singleton设计模式
    public class NHibernateHelper
    {
        
//Eagerly Initialize,确保线程同步
        private static ISessionFactory _sessionFactory = 
            
new Configuration().Configure().BuildSessionFactory();

        
private NHibernateHelper()
        {
        }

        
public static ISession OpenSession()
        {
            
return _sessionFactory.OpenSession();
        }
    }
}
(5)Domain Class
  在Domain目录下面创建两个实体类,代码非常简单,分别如下:
Department.cs:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->namespace NHibernatePractice2.Domain
{
    
public class Department
    {
        
public virtual int Id { getset; }
        
public virtual String Name { getset; }

        
//关联集合
        public virtual IList<Employee> Employees { getset; }

    }
}
Employee.cs:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->namespace NHibernatePractice2.Domain
{
    
public class Employee
    {
        
public virtual int Id { getset; }
        
public virtual String Name { getset; }

        
public virtual Department Department { getset; }
    }
}
(6)Mapping文件
  在Mappings目录下为Department与Employee分别创建映射文件(注意文件属性设置为“Embedded Resource”):
Department.hbm.xml:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--><?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
            assembly
="NHibernatePractice2"
            namespace
="NHibernatePractice2.Domain">
  
<class name="Department">
    
<id name="Id" >
      
<generator class="native" />
    
</id>
    
<property name="Name"/>
    
    
<!-- 设置fetch="join"抓取策略(global fetching strategy)-->
    
<!--该策略会被HQL忽略,但是不会被Get、Criteria忽略-->
    
<bag name="Employees" inverse="true" fetch="join">
      
<key column="DepartmentId" />
      
<one-to-many class="Employee"/>
    
</bag>

    
<!--若设置成lazy="false"(global fetch plan)-->
    
<!--则都不会被HQL、Get、Criteria忽略-->
    
<!--<bag name="Employees" inverse="true" lazy="false">
      <key column="DepartmentId" />
      <one-to-many class="Employee"/>
    </bag>
-->

  
</class>
</hibernate-mapping>
Employee.hbm.xml:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--><?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
            assembly
="NHibernatePractice2"
            namespace
="NHibernatePractice2.Domain">
  
<class name="Employee">
    
<id name="Id" >
      
<generator class="native" />
    
</id>
    
<property name="Name"/>
    
    
<many-to-one name="Department" class ="Department" column="DepartmentId" foreign-key="FK_Employee_Department" not-null="true"/>
  
  
</class>
</hibernate-mapping>

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--><?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
            assembly
="NHibernatePractice2"
            namespace
="NHibernatePractice2.Domain">
  
<class name="Employee">
    
<id name="Id" >
      
<generator class="native" />
    
</id>
    
<property name="Name"/>
    
    
<many-to-one name="Department" class ="Department" column="DepartmentId" foreign-key="FK_Employee_Department" not-null="true"/>
  
  
</class>
</hibernate-mapping>
  大家注意到,我在Department.hbm.xml文件中将关联集合已经设置为fetch="join"
(7)测试类NHibernatePractice2Test.cs
  在Test目录下创建测试类NHibernatePractice2Test.cs,代码如下:
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->namespace NHibernatePractice2.Test
{
    [TestFixture]
    
public class NHibernatePractice2Test
    {
        
//要查找的Department数据的Id=1
        int departmentId = 1;
        
        [Test]
        
//测试Get方法获取数据
        public void TestGet()
        {
            Department fromDb 
= null;

            
//如果mapping文件中设置了fetch="join",
            
//Get查询会在一条sql中同时获取Department以及所属的Employee
            using (ISession session = NHibernateHelper.OpenSession())
            
using (ITransaction transaction = session.BeginTransaction())
            {
                
//调用Get方法
                fromDb = session.Get<Department>(departmentId);
                
                transaction.Commit();
            }

            Assert.IsNotNull(fromDb);
            
//由于上面的Get查询同时获取了Department以及所属的Employee,测试成功
            Assert.AreEqual(2, fromDb.Employees.Count);
        }

        [Test]
        
//测试Criteria获取数据
        public void TestGetByCriteria()
        {
            Department fromDb 
= null;

            
//如果mapping文件中设置了fetch="join",
            
//Criteria查询会在一条sql中同时获取Department以及所属的Employee
            using (ISession session = NHibernateHelper.OpenSession())
            
using (ITransaction transaction = session.BeginTransaction())
            {
                fromDb 
= session.CreateCriteria(typeof(Department))
                    .Add(Restrictions.Eq(
"Id", departmentId))
                    .UniqueResult
<Department>();

                transaction.Commit();
            }

            Assert.IsNotNull(fromDb);
            
//由于上面的Criteria查询同时获取了Department以及所属的Employee,测试成功
            Assert.AreEqual(2, fromDb.Employees.Count);
        }

        [Test]
        
//测试HQL获取数据
        public void TestGetByHQL()
        {
            Department fromDb 
= null;

            
//如果mapping文件中设置了fetch="join",
            
//HQL查询会忽略fetch="join",因此只获取Department,不会获取Employee
            using (ISession session = NHibernateHelper.OpenSession())
            
using (ITransaction transaction = session.BeginTransaction())
            {
                fromDb 
= session.CreateQuery("from Department d where Id=:id")
                    .SetInt32(
"id", departmentId)
                    .UniqueResult
<Department>();

                transaction.Commit();
            }

            Assert.IsNotNull(fromDb);
            
//由于上面HQL查询只获取Department,没有获取Employee(也就是关联集合Employees未被初始化),
            
//同时session已经关闭(transaction.Commit),所以访问关联集合fromDb.Employees将抛出异常NHibernate.LazyInitializationException
            Assert.AreEqual(2, fromDb.Employees.Count);
        }
    }
}
  由于例子本身就非常简单,再加上已对代码进行了详细的注释,所以我这里只做个简单的介绍。3个测试方法TestGet()、TestGetByCriteria()、TestGetByHQL(),分别测试NHibernate的Get、Criteria和HQL获取数据。这里要注意的是,每个测试方法在验证前都进行了transaction.Commit();,效果就是关闭当前session,使查询得到的实体数据处于detached状态。如果在获取Department数据时没有加载Employees集合数据,这样当验证Assert.AreEqual(2, fromDb.Employees.Count);时会抛出NHibernate.LazyInitializationException异常
6、测试结
  编译生成项目后,在NUnit中进行测试,其中TestGet()、TestGetByCriteria()测试成功,TestGetByHQL()测试失败(抛出NHibernate.LazyInitializationException异常:failed to lazily initialize a collection, no session or session was closed)得到的结果如下:
测试结果
  查看NHibernate为TestGet()、TestGetByCriteria()生成的sql,两者功能一样,在1条sql中对Department和Employee进行left outer join,由此验证了mapping文件的fetch="join"设置影响Get与Criteria
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->NHibernate: SELECT department0_.Id as Id0_1_, department0_.Name as Name0_1_, employees1_.DepartmentId as Departme3_3_, employees1_.Id as Id3_, employees1_.Id as Id1_0_, employees1_.Name as Name1_0_, employees1_.DepartmentId as Departme3_1_0_ 
FROM Department department0_ left outer join Employee employees1_ 
on department0_.Id=employees1_.DepartmentId 
WHERE department0_.Id=@p0
@p0 = '1'
  查看NHibernate为TestGetByHQL()生成的sql,没有进行join而只是查询Department,由此验证了HQL忽略mapping文件中的fetch="join"
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->NHibernate: select department0_.Id as Id0_, department0_.Name as Name0_ 
from Department department0_ 
where (Id=@p0 ); 
@p0 = '1'
7、测试验证lazy="true"对Get、Criteria、HQL都产生影响
  在Department.hbm.xml文件中删除关联集合的fetch="join"抓取策略设置,改成立即加载lazy="true",其他代码和文件都保持不变,然后重新编译生成项目(注意:必须要重新编译生成项目),发现3个测试方法都通过测试:
测试结果2
  并且NHibernate为每个测试方法都生成相同功能sql,每个测试方法都是2条sql,第1条是获取Department,由于设置了立即加载lazy="true",就会立即执行第2条sql获取关联的Employee
Code
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->NHibernate: SELECT department0_.Id as Id0_0_, department0_.Name as Name0_0_ 
FROM Department department0_ 
WHERE department0_.Id=@p0@p0 = '1'

NHibernate: 
SELECT employees0_.DepartmentId as Departme3_1_, employees0_.Id as Id1_, employees0_.Id as Id1_0_, employees0_.Name as Name1_0_, employees0_.DepartmentId as Departme3_1_0_
FROM Employee employees0_ 
WHERE employees0_.DepartmentId=@p0@p0 = '1'

  由此可见,lazy="true"对Get、Criteria、HQL都产生影响
8、其他抓取策略的验
  除了fetch="join"、lazy="true",还有fetch="subselect"(子查询抓取),batch-size="20"(批量抓取)等抓取策略设置。由于篇幅受限以及时间关系,就留给有兴趣的朋友自己动手验证了。不过,如果你直接在Department.hbm.xml中将抓取策略修改成fetch="subselect"或者batch-size="20",测试的结果将是3个测试方法都失败,其中的缘由你自己去找,我就给你个提示:fetch="subselect",batch-size="20"在获取关联数据时,都需要查取主数据的session不能关闭仍要保持打开状态。所以为了进行这类测试,需要修改代码。

三、网上资
  上面的整个过程验证了HQL与Criteria对mapping文件中设置的抓取策略的差异,接下来我们搜索一下网上关于此现象的资料,其中提到的书是这本经典Java Persistence with Hibernate ,大家可以看看电子版,而且国内也有影印版。

四、总结
  本文通过一个实例验证了HQL与Criteria对mapping文件中设置的抓取策略的差异现象,具体表现在:
1、在mapping文件中的lazy="true"设置(global fetch plan):对Get、Criteria、HQL都将产生影响;
2、在mapping文件中的fetch="join"设置(global fetching strategy):对Get、Criteria产生影响,对HQL不影响。至于fetch="subselect",batch-size="20",我还未测试过,所以不妄下结论,有兴趣的朋友可以去试试。
  总之,如果你不注意这些设置产生的诡秘差异,你就会像我一开始一样会被NHibernate耍了。我发誓,我不会再在mapping文件中进行这些设置,除非不设置天就会塌下来,呵呵。
  写得够多了,现在才明白写技术blog真是一件既费时又辛苦的事情……不过下次还会继续……

五、完整实例代码下
  实例源代码下载

<script type="text/javascript">// <![CDATA[ if ($ != jQuery) { $ = jQuery.noConflict(); } var isLogined = false; var cb_blogId = 15964; var cb_entryId = 1558999; var cb_blogApp = "dddd218"; var cb_blogUserGuid = "cd4a360b-63cf-dd11-9e4d-001cf0cd104b"; var cb_entryCreatedDate = '2009/9/3 14:41:00'; // ]]></script>