数据库查询性能的提升也是涉及到开发中的各个阶段,在开发中选用正确的查询方法无疑是最基础也最简单的。
使用正确的SQL语句可以在很大程度上提高系统的查询性能。获得同样数据而采用不同方式的SQL语句在性能上的差距可能是十分巨大的。
由于Hibernate是对JDBC的封装,SQL语句的产生都是动态由Hibernate自动完成的。Hibernate产生SQL语句的方式有两种:一种是通过开发人员编写的HQL语句来生成,另一种是依据开发人员对关联对象的访问来自动生成相应的SQL语句。
至于使用什么样的SQL语句可以获得更好的性能要依据数据库的结构以及所要获取数据的具体情况来进行处理。在确定了所要执行的SQL语句后,可以通过以下三个方面来影响Hibernate所生成的SQL语句:
● HQL语句的书写方法。
● 查询时所使用的查询方法。
● 对象关联时所使用的抓取策略。
在前面已经介绍过,执行数据查询功能的基本方法有两种:一种是得到单个持久化对象的get()方法和load()方法,另一种是Query对象的list()方法和iterator()方法。在开发中应该依据不同的情况选用正确的方法。
get()方法和load()方法的区别在于对二级缓存的使用上。load()方法会使用二级缓存,而get()方法在一级缓存没有找到的情况下会直接查询数据库,不会去二级缓存中查找。在使用中,对使用了二级缓存的对象进行查询时最好使用load()方法,以充分利用二级缓存来提高检索的效率。
list()方法和iterator()方法之间的区别可以从以下几个方面来进行比较。
● 执行的查询不同
list()方法在执行时,是直接运行查询结果所需要的查询语句,而iterator()方法则是先执行得到对象ID的查询,然后再根据每个ID值去取得所要查询的对象。因此,对于list()方式的查询通常只会执行一个SQL语句,而对于iterator()方法的查询则可能需要执行N+1条SQL语句(N为结果集中的记录数)。
iterator()方法只是可能执行N+1条数据,具体执行SQL语句的数量取决于缓存的情况以及对结果集的访问情况。
● 缓存的使用
list()方法只能使用二级缓存中的查询缓存,而无法使用二级缓存对单个对象的缓存(但是会把查询出的对象放入二级缓存中)。所以,除非重复执行相同的查询操作,否则无法利用缓存的机制来提高查询的效率。
iterator()方法则可以充分利用二级缓存,在根据ID检索对象的时候会首先到缓存中查找,只有在找不到的情况下才会执行相应的查询语句。所以,缓存中对象的存在与否会影响到SQL语句的执行数量。
● 对于结果集的处理方法不同
list()方法会一次获得所有的结果集对象,而且它会依据查询的结果初始化所有的结果集对象。这在结果集非常大的时候必然会占据非常多的内存,甚至会造成内存溢出情况的发生。
iterator()方法在执行时不会一次初始化所有的对象,而是根据对结果集的访问情况来初始化对象。因此在访问中可以控制缓存中对象的数量,以避免占用过多缓存,导致内存溢出情况的发生。使用iterator()方法的另外一个好处是,如果只需要结果集中的部分记录,那么没有被用到的结果对象根本不会被初始化。所以,对结果集的访问情况也是调用iterator()方法时执行数据库SQL语句多少的一个因素。
所以,在使用Query对象执行数据查询时应该从以上几个方面去考虑使用何种方法来执行数据库的查询操作。
所谓抓取策略(fetching strategy)是指当应用程序需要利用关联关系进行对象获取的时候,Hibernate获取关联对象的策略。抓取策略可以在O/R映射的元数据中声明,也可以在特定的HQL或条件查询中声明。
Hibernate 3定义了以下几种抓取策略。
● 连接抓取(Join fetching)
连接抓取是指Hibernate在获得关联对象时会在SELECT语句中使用外连接的方式来获得关联对象。
● 查询抓取(Select fetching)
查询抓取是指Hibernate通过另外一条SELECT语句来抓取当前对象的关联对象的方式。这也是通过外键的方式来执行数据库的查询。与连接抓取的区别在于,通常情况下这个SELECT语句不是立即执行的,而是在访问到关联对象的时候才会执行。
● 子查询抓取(Subselect fetching)
子查询抓取也是指Hibernate通过另外一条SELECT语句来抓取当前对象的关联对象的方式。与查询抓取的区别在于它所采用的SELECT语句的方式为子查询,而不是通过外连接。
● 批量抓取(Batch fetching)
批量抓取是对查询抓取的优化,它会依据主键或者外键的列表来通过单条SELECT语句实现管理对象的批量抓取。
以上介绍的是Hibernate 3所提供的抓取策略,也就是抓取关联对象的手段。为了提升系统的性能,在抓取关联对象的时机上,还有以下一些选择。
● 立即抓取(Immediate fetching)
立即抓取是指宿主对象被加载时,它所关联的对象也会被立即加载。
● 延迟集合抓取(Lazy collection fetching)
延迟集合抓取是指在加载宿主对象时,并不立即加载它所关联的对象,而是到应用程序访问关联对象的时候才抓取关联对象。这是集合关联对象的默认行为。
● 延迟代理抓取(Lazy proxy fetching)
延迟代理抓取是指在返回单值关联对象的情况下,并不在对其进行get操作时抓取,而是直到调用其某个方法的时候才会抓取这个对象。
● 延迟属性加载(Lazy attribute fetching)
延迟属性加载是指在关联对象被访问的时候才进行关联对象的抓取。
介绍了Hibernate所提供的关联对象的抓取方法和抓取时机,这两个方面的因素都会影响Hibernate的抓取行为,最重要的是要清楚这两方面的影响是不同的,不要将这两个因素混淆,在开发中要结合实际情况选用正确的抓取策略和合适的抓取时机。
抓取时机的选择
在Hibernate 3中,对于集合类型的关联在默认情况下会使用延迟集合加载的抓取时机,而对于返回单值类型的关联在默认情况下会使用延迟代理抓取的抓取时机。
对于立即抓取在开发中很少被用到,因为这很可能会造成不必要的数据库操作,从而影响系统的性能。当宿主对象和关联对象总是被同时访问的时候才有可能会用到这种抓取时机。另外,使用立即连接抓取可以通过外连接来减少查询SQL语句的数量,所以,也会在某些特殊的情况下使用。
然而,延迟加载又会面临另外一个问题,如果在Session关闭前关联对象没有被实例化,那么在访问关联对象的时候就会抛出异常。处理的方法就是在事务提交之前就完成对关联对象的访问。
所以,在通常情况下都会使用延迟的方式来抓取关联的对象。因为每个立即抓取都会导致关联对象的立即实例化,太多的立即抓取关联会导致大量的对象被实例化,从而占用过多的内存资源。
抓取策略的选取
对于抓取策略的选取将影响到抓取关联对象的方式,也就是抓取关联对象时所执行的SQL语句。这就要根据实际的业务需求、数据的数量以及数据库的结构来进行选择了。
在这里需要注意的是,通常情况下都会在执行查询的时候针对每个查询来指定对其合适的抓取策略。指定抓取策略的方法如下所示:
User user = (User) session.createCriteria(User.class)
.setFetchMode("permissions", FetchMode.JOIN)
.add( Restrictions.idEq(userId) )
.uniqueResult();
本文介绍了查询性能提升的方法,关键是如何通过优化SQL语句来提升系统的查询性能。查询方法和抓取策略的影响也是通过执行查询方式和SQL语句的多少来改变系统的性能的。这些都属于开发人员所应该掌握的基本技能,避免由于开发不当而导致系统性能的低下。
在性能调整中,除了前面介绍的执行SQL语句的因素外,对于缓存的使用也会影响系统的性能。通常来说,缓存的使用会增加系统查询的性能,而降低系统增加、修改和删除操作的性能(因为要进行缓存的同步处理)。所以,开发人员应该能够正确地使用有效的缓存来提高数据查询的性能,而要避免滥用缓存而导致的系统性能变低。在采用缓存的时候也应该注意调整自己的检索策略和查询方法,这三者配合起来才可以达到最优的性能。
另外,事务的使用策略也会影响到系统的性能。选取正确的事务隔离级别以及使用正确的锁机制来控制数据的并发访问都会影响到系统的性能。
posted @ 2009-07-19 21:36 jadmin 阅读(1) 评论(0) 编辑
Hibernate是对JDBC的轻量级封装,因此在很多情况下Hibernate的性能比直接使用JDBC存取数据库要低。然而,通过正确的方法和策略,在使用Hibernate的时候还是可以非常接近直接使用JDBC时的效率的,并且,在有些情况下还有可能高于使用JDBC时的执行效率。
在进行Hibernate性能优化时,需要从以下几个方面进行考虑:
● 数据库设计调整。
● HQL优化。
● API的正确使用(如根据不同的业务类型选用不同的集合及查询API)。
● 主配置参数(日志、查询缓存、fetch_size、batch_size等)。
● 映射文件优化(ID生成策略、二级缓存、延迟加载、关联优化)。
● 一级缓存的管理。
● 针对二级缓存,还有许多特有的策略。
● 事务控制策略。
数据的查询性能往往是影响一个应用系统性能的主要因素。对查询性能的影响会涉及到系统软件开发的各个阶段,例如,良好的设计、正确的查询方法、适当的缓存都有利于系统性能的提升。
系统性能的提升设计到系统中的各个方面,是一个相互平衡的过程,需要在应用的各个阶段都要考虑。并且在开发、运行的过程中要不断地调整和优化才能逐步提升系统的性能。
posted @ 2009-07-19 21:30 jadmin 阅读(1) 评论(0) 编辑
在前面介绍了Hibernate的缓存技术以及基本的用法,在这里就具体的Hibernate所提供的查询方法与Hibernate缓存之间的关系做一个简单的总结。
在开发中,通常是通过两种方式来执行对数据库的查询操作的。一种方式是通过ID来获得单独的Java对象,另一种方式是通过HQL语句来执行对数据库的查询操作。下面就分别结合这两种查询方式来说明一下缓存的作用。
通过ID来获得Java对象可以直接使用Session对象的load()或者get()方法,这两种方式的区别就在于对缓存的使用上。
● load()方法
在使用了二级缓存的情况下,使用load()方法会在二级缓存中查找指定的对象是否存在。
在执行load()方法时,Hibernate首先从当前Session的一级缓存中获取ID对应的值,在获取不到的情况下,将根据该对象是否配置了二级缓存来做相应的处理。
如配置了二级缓存,则从二级缓存中获取ID对应的值,如仍然获取不到则还需要根据是否配置了延迟加载来决定如何执行,如未配置延迟加载则从数据库中直接获取。在从数据库获取到数据的情况下,Hibernate会相应地填充一级缓存和二级缓存,如配置了延迟加载则直接返回一个代理类,只有在触发代理类的调用时才进行数据库的查询操作。
在Session一直打开的情况下,并在该对象具有单向关联维护的时候,需要使用类似Session.clear(),Session.evict()的方法来强制刷新一级缓存。
● get()方法
get()方法与load()方法的区别就在于不会查找二级缓存。在当前Session的一级缓存中获取不到指定的对象时,会直接执行查询语句从数据库中获得所需要的数据。
在Hibernate中,可以通过HQL来执行对数据库的查询操作。具体的查询是由Query对象的list()和iterator()方法来执行的。这两个方法在执行查询时的处理方法存在着一定的差别,在开发中应该依据具体的情况来选择合适的方法。
● list()方法
在执行Query的list()方法时,Hibernate的做法是首先检查是否配置了查询缓存,如配置了则从查询缓存中寻找是否已经对该查询进行了缓存,如获取不到则从数据库中进行获取。从数据库中获取到后,Hibernate将会相应地填充一级、二级和查询缓存。如获取到的为直接的结果集,则直接返回,如获取到的为一些ID的值,则再根据ID获取相应的值(Session.load()),最后形成结果集返回。可以看到,在这样的情况下,list()方法也是有可能造成N次查询的。
查询缓存在数据发生任何变化的情况下都会被自动清空。
● iterator()方法
Query的iterator()方法处理查询的方式与list()方法是不同的,它首先会使用查询语句得到ID值的列表,然后再使用Session的load()方法得到所需要的对象的值。
在获取数据的时候,应该依据这4种获取数据方式的特点来选择合适的方法。在开发中可以通过设置show_sql选项来输出Hibernate所执行的SQL语句,以此来了解Hibernate是如何操作数据库的。
posted @ 2009-07-19 21:29 jadmin 阅读(2) 评论(0) 编辑
查询缓存是专门针对各种查询操作进行缓存。查询缓存会在整个SessionFactory的生命周期中起作用,存储的方式也是采用key-value的形式来进行存储的。
查询缓存中的key是根据查询的语句、查询的条件、查询的参数和查询的页数等信息组成的。而数据的存储则会使用两种方式,使用SELECT语句只查询实体对象的某些列或者某些实体对象列的组合时,会直接缓存整个结果集。而对于查询结果为某个实体对象集合的情况则只会缓存实体对象的ID值,以达到缓存空间可以共用,节省空间的目的。
在使用查询缓存时,除了需要设置hibernate.cache.provider_class参数来启动二级缓存外,还需要通过hibernate.cache.use_query_cache参数来启动对查询缓存的支持。
另外需要注意的是,查询缓存是在执行查询语句的时候指定缓存的方式以及是否需要对查询的结果进行缓存。
下面就来了解一下查询缓存的使用方法及作用。
修改Hibernate配置文件
首先需要修改Hibernate的配置文件,增加hibernate.cache.use_query_cache参数的配置。配置方法如下:
Hibernate配置文件的详细内容请参考配套光盘中的hibernate\src\cn\hxex\ hibernate\cache\hibernate.cfg.xml文件。
编写主测试程序
由于这是在前面二级缓存例子的基础上来开发的,所以,对于EHCache的配置以及视图对象的开发和映射文件的配置工作就都不需要再重新进行了。下面就来看一下主测试程序的实现方法,如清单14.11所示。
清单14.11 主程序的实现
……
public void run() {
SessionFactory sf = QueryCacheMain.getSessionFactory();
Session session = sf.getCurrentSession();
session.beginTransaction();
Query query = session.createQuery( "from User" );
Iterator it = query.setCacheable( true ).list().iterator();
while( it.hasNext() ) {
System.out.println( it.next() );
}
User user = (User)session.get( User.class, "1" );
System.out.println( user );
session.getTransaction().commit();
}
public static void main(String[] args) {
QueryCacheMain main1 = new QueryCacheMain();
main1.start();
try {
Thread.sleep( 2000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
QueryCacheMain main2 = new QueryCacheMain();
main2.start();
}
}
主程序在实现的时候采用了多线程的方式来运行。首先将“from User”查询结果进行缓存,然后再通过ID取得对象来检查是否对对象进行了缓存。另外,多个线程的执行可以看出对于进行了缓存的查询是不会执行第二次的。
运行测试主程序
接着就来运行测试主程序,其输出结果应该如下所示:
Hibernate: select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_ from USERINFO user0_
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
通过上面的执行结果可以看到,在两个线程执行中,只执行了一个SQL查询语句。这是因为根据ID所要获取的对象在前面的查询中已经得到了,并进行了缓存,所以没有再次执行查询语句。
posted @ 2009-07-19 21:25 jadmin 阅读(1) 评论(0) 编辑
与Session相对的是,SessionFactory也提供了相应的缓存机制。SessionFactory缓存可以依据功能和目的的不同而划分为内置缓存和外置缓存。
SessionFactory的内置缓存中存放了映射元数据和预定义SQL语句,映射元数据是映射文件中数据的副本,而预定义SQL语句是在Hibernate初始化阶段根据映射元数据推导出来的。SessionFactory的内置缓存是只读的,应用程序不能修改缓存中的映射元数据和预定义SQL语句,因此SessionFactory不需要进行内置缓存与映射文件的同步。
SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插件。外置缓存的数据是数据库数据的副本,外置缓存的介质可以是内存或者硬盘。SessionFactory的外置缓存也被称为Hibernate的二级缓存。
Hibernate的二级缓存的实现原理与一级缓存是一样的,也是通过以ID为key的Map来实现对对象的缓存。
由于Hibernate的二级缓存是作用在SessionFactory范围内的,因而它比一级缓存的范围更广,可以被所有的Session对象所共享。
Hibernate的二级缓存同一级缓存一样,也是针对对象ID来进行缓存。所以说,二级缓存的作用范围是针对根据ID获得对象的查询。
二级缓存的工作可以概括为以下几个部分:
● 在执行各种条件查询时,如果所获得的结果集为实体对象的集合,那么就会把所有的数据对象根据ID放入到二级缓存中。
● 当Hibernate根据ID访问数据对象的时候,首先会从Session一级缓存中查找,如果查不到并且配置了二级缓存,那么会从二级缓存中查找,如果还查不到,就会查询数据库,把结果按照ID放入到缓存中。
● 删除、更新、增加数据的时候,同时更新缓存。
Hibernate的二级缓存作为一个可插入的组件在使用的时候也是可以进行配置的,但并不是所有的对象都适合放在二级缓存中。
在通常情况下会将具有以下特征的数据放入到二级缓存中:
● 很少被修改的数据。
● 不是很重要的数据,允许出现偶尔并发的数据。
● 不会被并发访问的数据。
● 参考数据。
而对于具有以下特征的数据则不适合放在二级缓存中:
● 经常被修改的数据。
● 财务数据,绝对不允许出现并发。
● 与其他应用共享的数据。
在这里特别要注意的是对放入缓存中的数据不能有第三方的应用对数据进行更改(其中也包括在自己程序中使用其他方式进行数据的修改,例如,JDBC),因为那样Hibernate将不会知道数据已经被修改,也就无法保证缓存中的数据与数据库中数据的一致性。
在默认情况下,Hibernate会使用EHCache作为二级缓存组件。但是,可以通过设置hibernate.cache.provider_class属性,指定其他的缓存策略,该缓存策略必须实现org.hibernate.cache.CacheProvider接口。
通过实现org.hibernate.cache.CacheProvider接口可以提供对不同二级缓存组件的支持。
Hibernate内置支持的二级缓存组件如表14.1所示。
表14.1 Hibernate所支持的二级缓存组件
posted @ 2009-07-19 21:23 jadmin 阅读(0) 评论(0) 编辑
大家都知道,Hibernate是以JDBC为基础实现的持久层组件,因而其性能肯定会低于直接使用JDBC来访问数据库。因此,为了提高Hibernate的性能,在Hibernate组件中提供了完善的缓存机制来提高数据库访问的性能。
什么是缓存
缓存是介于应用程序和物理数据之间的,其作用是为了降低应用程序对物理数据访问的频次从而提高应用系统的性能。缓存思想的提出主要是因为对物理数据的访问效率要远远低于对内存的访问速度,因而采用了将部分物理数据存放于内存当中,这样可以有效地减少对物理数据的访问次数,从而提高系统的性能。
缓存广泛地存在于我们所接触的各种应用系统中,例如数据库系统、Windows操作系统等,在进行物理数据的访问时无一例外地都使用了缓存机制来提高操作的性能。
缓存内的数据是对物理数据的复制,因此一个缓存系统所应该包括的最基本的功能是数据的缓存和读取,同时在使用缓存的时候还要考虑缓存中的数据与物理数据的同步,也就是要保持两者是一致的。
缓存要求对数据的读写速度很高,因此,一般情况下会选用内存作为存储的介质。但如果内存有限,并且缓存中存放的数据量非常大时,也会用硬盘作为缓存介质。缓存的实现不仅仅要考虑存储的介质,还要考虑到管理缓存的并发访问和缓存数据的生命周期。
为了提高系统的性能,Hibernate也使用了缓存的机制。在Hibernate框架中,主要包括以下两个方面的缓存:一级缓存和二级缓存(包含查询缓存)。Hibernate中缓存的作用主要表现在以下两个方面:
● 通过主键(ID)加载数据的时候
● 延迟加载中
一级缓存
Hibernate的一级缓存是由Session提供的,因此它只存在于Session的生命周期中,也就是当Session关闭的时候该Session所管理的一级缓存也会立即被清除。
Hibernate的一级缓存是Session所内置的,不能被卸载,也不能进行任何配置。
一级缓存采用的是key-value的Map方式来实现的,在缓存实体对象时,对象的主关键字ID是Map的key,实体对象就是对应的值。所以说,一级缓存是以实体对象为单位进行存储的,在访问的时候使用的是主关键字ID。
虽然,Hibernate对一级缓存使用的是自动维护的功能,没有提供任何配置功能,但是可以通过Session中所提供的方法来对一级缓存的管理进行手工干预。Session中所提供的干预方法包括以下两种。
● evict() :用于将某个对象从Session的一级缓存中清除。
● clear() :用于将一级缓存中的对象全部清除。
在进行大批量数据一次性更新的时候,会占用非常多的内存来缓存被更新的对象。这时就应该阶段性地调用clear()方法来清空一级缓存中的对象,控制一级缓存的大小,以避免产生内存溢出的情况。具体的实现方法如清单14.8所示。
清单14.8 大批量更新时缓存的处理方法
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
Customer customer = new Customer(……);
session.save(customer);
if ( i % 20 == 0 ) {
//将本批插入的对象立即写入数据库并释放内存
session.flush();
session.clear();
}
}
tx.commit();
session.close();
posted @ 2009-07-19 21:18 jadmin 阅读(0) 评论(0) 编辑
并发控制
当数据库系统采用Read Committed隔离级别时,会导致不可重复读取和两次更新丢失的并发问题,可以在应用程序中采用锁机制来避免这类问题的产生。
从应用程序的角度上看,锁可以分为乐观锁和悲观锁两大类。
悲观锁
在多个客户端可能读取同一笔数据或同时更新一笔数据的情况下,必须要有访问控制的手段,防止同一个数据被修改而造成混乱,最简单的手段就是对数据进行锁定。在自己进行数据读取或更新等动作时,锁定其他客户端不能对同一笔数据进行任何的动作。
悲观锁(Pessimistic Locking),如其名称所示,悲观地认定每次资料存取时,其他的客户端也会存取同一笔数据,因此将会锁住该笔数据,直到自己操作完成后再解除锁。
悲观锁假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其他的客户不能对数据进行存取。对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多访问的机器,如果每一次读取数据都造成锁定,其后继的存取就必须等待,这将造成效能上的问题,造成后继使用者的长时间等待。
悲观锁通常透过系统或数据库本身的功能来实现,依赖系统或数据库本身提供的锁机制。Hibernate即是如此,可以利用Query或Criteria的setLockMode()方法来设定要锁定的表或列及其锁模式,可设定的锁模式有以下几个。
LockMode.UPGRADE:利用数据库的for update子句进行锁定。
LockMode.UPGRADE_NOWAIT:使用for update nowait子句进行锁定,在Oracle数据库中使用。
下面来实现一个简单的例子,测试一下采用悲观锁时数据库是如何进行操作的。
首先来完成一个实体对象——User,该对象包含了id,name和age三个属性,实现的方法如清单14.1所示。
清单14.1 User对象的实现
package cn.hxex.hibernate.lock;
public class User {
private String id;
private String name;
private Integer age;
// 省略了getter和setter方法
……
}
接下来就是映射文件的配置,由于该映射文件没有涉及到任何与其他对象的关联配置,所以实现的方法也非常简单,代码如清单14.2所示。
清单14.2 User映射文件的实现
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
另外一件重要的工作就是Hibernate的配置文件了,在这个配置文件中包含了连接数据库的参数以及其他一些重要的参数,实现的方法如清单14.3所示。
清单14.3 Hibernate配置文件的实现
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
jdbc:mysql://localhost:3306/lockdb?useUnicode=true&characterEncoding=utf8&autoReconnect=true&autoReconnectForPools=true
com.mysql.jdbc.Driver
org.hibernate.dialect.MySQLDialect
最后要实现的就是测试主程序了,在测试主程序中包含了Hibernate的初始化代码以及悲观锁的测试方法。测试主程序的实现方法如清单14.4所示。
清单14.4 测试主程序的实现
package cn.hxex.hibernate.lock;
import java.net.URL;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class LockMain {
private static Log log = LogFactory.getLog( LockMain.class );
// 静态Configuration和SessionFactory对象的实例(全局唯一的)
private static Configuration configuration;
private static SessionFactory sessionFactory;
static
{
// 从默认的配置文件创建SessionFactory
try
{
URL configURL = ClassLoader.getSystemResource(
"cn/hxex/hibernate/lock/hibernate.cfg.xml" );
// 创建默认的Configuration对象的实例
configuration = new Configuration();
// 读取hibernate.properties或者hibernate.cfg.xml文件
configuration.configure( configURL );
// 使用静态变量来保持SessioFactory对象的实例
sessionFactory = configuration.buildSessionFactory();
}
catch (Throwable ex)
{
// 输出异常信息
log.error("Building SessionFactory failed.", ex);
ex.printStackTrace();
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
public void testPessimisticLock() {
SessionFactory sf = LockMain.getSessionFactory();
Session session = sf.getCurrentSession();
session.beginTransaction();
Query query = session.createQuery("from User user");
query.setLockMode("user", LockMode.UPGRADE);
List users = query.list();
for( int i=0; i System.out.println( users.get( i ) ); } session.getTransaction().commit(); } public static void main(String[] args) { LockMain main = new LockMain(); main.testPessimisticLock(); } } 在上面的清单中,testPessimisticLock()方法就是测试悲观锁的方法,该方法在执行查询之前通过Query对象的setLockMode()方法设置了访问User对象的模式,这样,这个程序在执行的时候就会使用以下的SQL语句: select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_ from USERINFO user0_ for update 除了Query对象外,也可以在使用Session的load()或是lock()时指定锁模式。 除了前面所提及的两种锁模式外,还有三种Hibernate内部自动对数据进行加锁的模式,但它的处理是与数据库无关的。 LockMode.WRITE:在insert或update时进行锁定,Hibernate会在调用save()方法时自动获得锁。 LockMode.READ:在读取记录时Hibernate会自动获得锁。 LockMode.NONE:没有锁。 如果数据库不支持所指定的锁模式,Hibernate会选择一个合适的锁替换,而不是抛出一个异常。 乐观锁 乐观锁(Optimistic Locking)认为资料的存取很少发生同时存取的问题,因而不做数据库层次上的锁定。为了维护正确的数据,乐观锁是使用应用程序上的逻辑来实现版本控制的。 在使用乐观锁策略的情况下,数据不一致的情况一旦发生,有几个解决方法,一种是先更新为主,一种是后更新为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁。 Hibernate中通过检查版本号来判断数据是否已经被其他人所改动,这也是Hibernate所推荐的方式。在数据库中加入一个version字段记录,在读取数据时连同版本号一同读取,并在更新数据时比较版本号与数据库中的版本号,如果等于数据库中的版本号则予以更新,并递增版本号,如果小于数据库中的版本号就抛出异常。 下面就来在前面例子的基础上进行Hibernate乐观锁的测试。 首先需要修改前面所实现的业务对象,在其中增加一个version属性,用来记录该对象所包含数据的版本信息,修改后的User对象如清单14.5所示。 清单14.5 修改后的User对象 package cn.hxex.hibernate.lock; public class User { private String id; private Integer version; // 增加版本属性 private String name; private Integer age; // 省略了getter和setter方法 …… } 然后是修改映射文件,增加version属性的配置。在这里需要注意的是,这里的version属性应该使用专门的 修改后的映射文件如清单14.6所示。 清单14.6 修改后的映射文件
"-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 接下来还要进行测试主程序的修改。由于需要模拟两个人同时修改同一个记录的情况,所以在这里需要将主程序修改为是可以多线程执行的,然后在run()方法中,调用对User对象的修改程序。 实现后的主测试程序如清单14.7所示。 清单14.7 修改后的测试主程序 package cn.hxex.hibernate.lock; import java.net.URL; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.LockMode; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.Configuration; public class LockMain extends Thread{ …… public void testOptimisticLock() { SessionFactory sf = LockMain.getSessionFactory(); Session session = sf.openSession(); Transaction tx = session.beginTransaction(); User userV1 = (User)session.load( User.class, "1" ); // 等第二个进程执行 try { sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); } userV1.setAge(new Integer(32)); tx.commit(); session.close(); } public void run() { testOptimisticLock(); } public static void main(String[] args) { // LockMain main = new LockMain(); // main.testPessimisticLock(); LockMain main1 = new LockMain(); main1.start(); LockMain main2 = new LockMain(); main2.start(); } } 最后,执行测试主程序,在控制台中应该看到类似下面的输出: Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=? Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=? Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=? Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=? 2006-10-3 21:27:20 org.hibernate.event.def.AbstractFlushingEventListener performExecutions 严重: Could not synchronize database state with session org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [cn.hxex.hibernate.lock.User#1] …… 在Hibernate所执行的UPDATE语句中可以看到,version字段是作为更新的条件来执行的。对于第二个进程来说,由于数据库中的记录已经被第一个进程更新(更新的同时会导致version自动增加),就必然会导致第二个进程操作的失败。Hibernate正是利用这种机制来避免两次更新问题的出现。 posted @ 2009-07-19 21:11 jadmin 阅读(6) 评论(0) 编辑
在现在的B/S体系结构的软件开发中,对于数据库事务处理中最常使用的方式是每个用户请求一个事务。也就是说,当服务器端接收到一个用户请求后,会开始一个新的事务,直到对用户请求的所有处理都进行完毕并且完成了响应用户请求的所有输出之后才会关闭这个事务。
对于使用Hibernate实现持久化功能的系统来说,事务的处理是这样的:服务器端在接收到用户的请求后,会创建一个新的Hibernate Session对象,然后通过该Session对象开始一个新的事务并且之后所有对数据库的操作都通过该Session对象来进行。最后,完成将响应页面发送到客户端的工作后再提交事务并且关闭Session。
Session的对象是轻型的,非线程安全的,所以在每次用户请求时创建,请求处理完毕后丢弃。
那么,该如何实现这种方式的事务处理呢?处理的难点在于如何在业务处理之前创建Session并开始事务以及在业务处理之后提交事务并关闭Session。对于现在的Web应用来说,通常情况下是通过ServletFilter来完成事务处理的操作。这样,就可以轻松地实现在用户请求到达服务器端的时候创建Session并开始事务,而服务器端响应处理结束之前提交事务并关闭Session。
另外一个问题是,在ServletFilter中创建的Session是如何传递给业务处理方法中的呢?处理的方法是通过一个ThreadLocal变量来把创建的Session对象绑定到处理用户请求的线程上去,这样就可以使任何的业务处理方法可以轻松得到Session对象。
Hibernate中事务处理的具体方法可以参照前面的网络博客的实例。
但是这种事务处理的方式还是会遇到一些问题,其中最突出的就是更新冲突的问题。例如,某个操作人员进入了用户信息的修改页面,在经过一段时间的对用户信息的修改后,进行提交操作,而与此同时可能会有另外一个操作人员也进行了相同的操作,这样在处理提交的时候就会产生冲突。
产生这个冲突的原因在于在开发中需要使用多个数据库事务来实现一个应用事务。也就是说,在应用程序层,应该将读取用户信息、显示修改页面以及用户提交工作来作为一个事务进行处理,在处理的过程中应该避免其他操作人员进行类似的操作。
回想前面的介绍,我们对于数据库事务所采取的策略是每个用户请求一个事务,而上面的业务处理则至少需要两个请求才能完成。这样,两者之间就存在着一定的矛盾,这也就导致了不可重复读取和两次更新问题的发生。
为了解决并发中数据访问的问题,通常会采用锁的机制来实现数据访问的排他性,从而避免两次更新问题的发生。
posted @ 2009-07-19 21:07 jadmin 阅读(0) 评论(0) 编辑
为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
● 未授权读取(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
● 授权读取(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
● 可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
● 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
通过前面的介绍已经知道,通过选用不同的隔离等级就可以在不同程度上避免前面所提及的在事务处理中所面临的各种问题。所以,数据库隔离级别的选取就显得尤为重要,在选取数据库的隔离级别时,应该注意以下几个处理的原则:
首先,必须排除“未授权读取”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚将会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未授权读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。
其次,绝大部分应用都无须使用“序列化”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用悲观锁,这样强行使所有事务都序列化执行。
剩下的也就是在“授权读取”和“可重复读取”之间选择了。我们先考虑可重复读取。如果所有的数据访问都是在统一的原子数据库事务中,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性(第二个事务更新丢失问题)。这是一个非常重要的问题,但是使用可重复读取并不是解决问题的唯一途径。
假设使用了“版本数据”,Hibernate会自动使用版本数据。Hibernate的一级Session缓存和版本数据已经为你提供了“可重复读取隔离”绝大部分的特性。特别是,版本数据可以防止二次更新丢失的问题,一级Session缓存可以保证持久载入数据的状态与其他事务对数据的修改隔离开来,因此如果使用对所有的数据库事务采用授权读取隔离和版本数据是行得通的。
“可重复读取”为数据库查询提供了更好的效率(仅对那些长时间的数据库事务),但是由于幻影读取依然存在,因此没必要使用它(对于Web应用来说,一般也很少在一个数据库事务中对同一个表查询两次)。
也可以同时考虑选择使用Hibernate的二级缓存,它可以如同底层的数据库事务一样提供相同的事务隔离,但是它可能弱化隔离。假如在二级缓存大量使用缓存并发策略,它并不提供重复读取语义(例如,后面章节中将要讨论的读写,特别是非严格读写),很容易可以选择默认的隔离级别:因为无论如何都无法实现“可重复读取”,因此就更没有必要拖慢数据库了。另一方面,可能对关键类不采用二级缓存,或者采用一个完全的事务缓存,提供“可重复读取隔离”。那么在业务中需要使用到“可重复读取”吗?如果你喜欢,当然可以那样做,但更多的时候并没有必要花费这个代价。
posted @ 2009-07-19 21:04 jadmin 阅读(0) 评论(0) 编辑
数据库的事务处理是在进行数据库应用开发中必须进行处理的一个问题。那么对于选择Hibernate作为持久层组件,了解Hibernate的事务处理机制就显得尤为重要了。
事务的基本概念
事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银行转账工作:从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要么都不执行。所以,应该把它们看成一个事务。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。
针对上面的描述可以看出,事务的提出主要是为了解决并发情况下保持数据一致性的问题。
事务具有以下4个基本特征。
● Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
● Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
● Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
● Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。
数据库肯定是要被广大客户所共享访问的,那么在数据库操作过程中很可能出现以下几种不确定情况。
● 更新丢失(Lost update):两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
● 脏读取(Dirty Reads):一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。
● 不可重复读取(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。例如,在两次读取的中途,有另外一个事务对该行数据进行了修改,并提交。
● 两次更新问题(Second lost updates problem):无法重复读取的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。
● 虚读(Phantom Reads):事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
posted @ 2009-07-19 21:04 jadmin 阅读(2) 评论(0) 编辑
HibernateTemplate还提供了一种更加灵活的方式来操作数据库,通过这种方式可以完全使用Hibernate的操作方式。HibernateTemplate的灵活访问方式可通过如下两个方法完成:
● Object execute(HibernateCallback action)。
● List execute(HibernateCallback action)。
这两个方法都需要一个HibernateCallback的实例,HibernateCallback实例可在任何有效的Hibernate数据访问中使用。程序开发者通过HibernateCallback,可以完全使用Hibernate灵活的方式来访问数据库,解决Spring封装Hibernate后灵活性不足的缺陷。
HibernateCallback是一个接口,该接口包含一个方法doInHibernate(org.hibernate. Session session),该方法只有一个参数Session。在开发中提供HibernateCallback实现类时,必须实现接口里包含的doInHibernate方法,在该方法体内即可获得Hibernate Session的引用,一旦获得了Hibernate Session的引用,就可以完全以Hibernate的方式进行数据库访问。
注意:doInHibernate方法内可以访问Session,该Session对象是绑定在该线程的Session实例。该方法内的持久层操作,与不使用Spring时的持久层操作完全相同。这保证了对于复杂的持久层访问,依然可以使用Hibernate的访问方式。
下面的代码对HibernateDaoSupport类进行扩展(虽然Spring 2.0的HibernateTemplate提供了一个分页方法setMaxResults,但仅此一个方法依然不能实现分页查询),这种扩展主要是为该类增加了3个分页查询的方法,分页查询时必须直接调用Hibernate的Session完成,因此,必须借助于HibernateCallBack的帮助。
public class YeekuHibernateDaoSupport extends HibernateDaoSupport
{
/**
* 使用hql 语句进行分页查询操作
* @param hql 需要查询的hql语句
* @param offset 第一条记录索引
* @param pageSize 每页需要显示的记录数
* @return 当前页的所有记录
*/
public List findByPage(final String hql,
final int offset, final int pageSize)
{
//HibernateDaoSupport已经包含了getHibernateTemplate()方法
List list = getHibernateTemplate().executeFind(new
HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
//该方法体内以Hibernate方法进行持久层访问
{
List result = session.createQuery(hql)
.setFirstResult(offset)
.setMaxResults(pageSize)
.list();
return result;
}
});
return list;
}
/**
* 使用hql 语句进行分页查询操作
* @param hql 需要查询的hql语句
* @param value 如果hql有一个参数需要传入,value就是传入的参数
* @param offset 第一条记录索引
* @param pageSize 每页需要显示的记录数
* @return 当前页的所有记录
*/
public List findByPage(final String hql , final Object value ,
final int offset, final int pageSize)
{
List list = getHibernateTemplate().executeFind(new
HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
{
//下面查询的是最简单的Hiberante HQL查询
List result = session.createQuery(hql)
.setParameter(0, value)
.setFirstResult(offset)
.setMaxResults(pageSize)
.list();
return result;
}
});
return list;
}
/**
* 使用hql 语句进行分页查询操作
* @param hql 需要查询的hql语句
* @param values 如果hql有多个参数需要传入,values就是传入的参数数组
* @param offset 第一条记录索引
* @param pageSize 每页需要显示的记录数
* @return 当前页的所有记录
*/
public List findByPage(final String hql, final Object[] values,
final int offset, final int pageSize)
{
List list = getHibernateTemplate().executeFind(new
HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
{
Query query = session.createQuery(hql);
for (int i = 0 ; i < values.length ; i++)
{
query.setParameter( i, values[i]);
}
List result = query.setFirstResult(offset)
.setMaxResults(pageSize)
.list();
return result;
}
});
return list;
}
}
在上面的代码实现中,直接使用了getHibernateTemplate()方法,这个方法由Hibernate- DaoSupport提供。而YeekuHibernateDaoSupport是HibernateDaoSupport的子类,因此,可以直接使用该方法。
当实现doInHibernate(Session session)方法时,完全以Hibernate的方式进行数据库访问,这样保证了Hibernate进行数据库访问的灵活性。
注意:Spring提供的XxxTemplate和XxxCallBack互为补充,二者体现了Spring框架设计的用心良苦:XxxTemplate对通用操作进行封装,而XxxCallBack解决了封装后灵活性不足的缺陷。
为了实现DAO组件,Spring提供了大量的XxxDaoSupport类,这些DAO支持类对于实现DAO组件大有帮助,因为这些DAO支持类已经完成了大量基础性工作。
Spring为Hibernate的DAO提供了工具类HibernateDaoSupport。该类主要提供如下两个方法以方便DAO的实现:
● public final HibernateTemplate getHibernateTemplate()。
● public final void setSessionFactory(SessionFactory sessionFactory)。
其中,setSessionFactory方法可用于接收Spring的ApplicationContext的依赖注入,可接收配置在Spring的SessionFactory实例,getHibernateTemplate方法用于返回通过SessionFactory产生的HibernateTemplate实例,持久层访问依然通过HibernateTemplate实例完成。
下面实现的DAO组件继承了Spring提供的HibernateDaoSupport类,依然实现了PersonDao接口,其功能与前面提供的PersonDao实现类完全相同。其代码如下:
public class PersonDaoHibernate extends HibernateDaoSupport implements PersonDao
{
/**
* 加载人实例
* @param id 需要加载的Person实例的主键值
* @return 返回加载的Person实例
*/
public Person get(int id)
{
return (Person)getHibernateTemplate().get(Person.class, new
Integer(id));
}
/**
* 保存人实例
* @param person 需要保存的Person实例
*/
public void save(Person person)
{
getHibernateTemplate().save(person);
}
/**
* 修改Person实例
* @param person 需要修改的Person实例
*/
public void update(Person person)
{
getHibernateTemplate().update(person);
}
/**
* 删除Person实例
* @param id 需要删除的Person的id
*/
public void delete(int id)
{
getHibernateTemplate().delete(getHibernateTemplate().
get(Person.class, new Integer(id)));
}
/**
* 删除Person实例
* @param person 需要删除的Person实例
*/
public void delete(Person person)
{
getHibernateTemplate().delete(person);
}
/**
* 根据用户名查找Person
* @param name 用户名
* @return 用户名对应的全部用户
*/
public List findByPerson(String name)
{
return getHibernateTemplate().find("from Person p where p.name
like ?" , name);
}
/**
* 返回全部的Person实例
* @return 全部的Person实例
*/
public List findAllPerson()
{
return getHibernateTemplate().find("from Person ");
}
}
上面的代码与前面的PersonDAOImpl对比会发现,代码量大大减少。事实上,DAO的实现依然借助于HibernateTemplate的模板访问方式,只是HibernateDaoSupport将依赖注入SessionFactory的工作已经完成,获取HibernateTemplate的工作也已完成。该DAO的配置必须依赖于SessionFactory,配置文件与前面部署DAO组件的方式完全相同,此处不再赘述。
在继承HibernateDaoSupport的DAO实现里,Hibernate Session的管理完全不需要打开代码,而由Spring来管理。Spring会根据实际的操作,采用“每次事务打开一次session”的策略,自动提高数据库访问的性能。
至此为止,J2EE应用所需要的各种组件都已经出现了,从MVC层的控制器组件,到业务逻辑组件,以及持久层的DAO组件,已经全部成功实现。应用程序代码并未将这些组件耦合在一起,代码中都是面向接口编程,因此必须利用Spring的IoC容器将他们组合在一起。
从用户角度来看,用户发出HTTP请求,当MVC框架的控制器组件拦截到用户请求时,将调用系统的业务逻辑组件,而业务逻辑组件则调用系统的DAO组件,而DAO组件则依赖于SessionFactory和DataSource等底层组件实现数据库访问。
从系统实现角度来看,IoC容器先创建SessionFactory和DataSource等底层组件,然后将这些底层组件注入给DAO组件,提供一个完整的DAO组件,并将此DAO组件注入给业务逻辑组件,从而提供一个完整的业务逻辑组件,而业务逻辑组件又被注入给控制器组件,控制器组件负责拦截用户请求,并将处理结果呈现给用户——这一系列的衔接都由Spring的IoC容器提供实现。
下面给出关于如何在容器中配置J2EE组件的大致模板,其模板代码如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> ... ...
MySQLDialect
在上面的配置文件中,同时配置了控制器Bean、业务逻辑组件Bean、DAO组件Bean以及一些基础资源Bean。各组件的组织被解耦到配置文件中,而不是在代码层次的低级耦合。
当客户端的HTTP请求向/login.do发送请求时,将被容器中的lee.LoginAction拦截,LoginAction调用myService Bean,myService Bean则调用personDao等系列DAO组件,整个流程将系统中的各组件有机地组织在一起。
注意:在实际应用中,很少会将DAO组件、业务逻辑组件以及控制组件都配置在同一个文件中。而是在不同配置文件中,配置相同一组J2EE应用组件。
在上面的配置文件中,部署了控制器组件、业务逻辑组件、DAO组件,几乎可以形成一个完整的J2EE应用。但有一个小小的问题:事务控制。系统没有任何事务逻辑,没有事务逻辑的应用是不可想象的。
Spring提供了非常简洁的声明式事务控制,只需要在配置文件中增加事务控制片段,业务逻辑代码无须任何改变。Spring的声明式事务逻辑,甚至支持在不同事务策略之间切换。
配置Spring声明式事务时,通常推荐使用BeanNameAutoProxyCreator自动创建事务代理。通过这种自动事务代理的配置策略,增加业务逻辑组件,只需要在BeanNameAutoProxyCreator Bean配置中增加一行即可,从而避免了增量式配置。
在上面的配置模板文件中增加如下配置片段,系统的myService业务逻辑组件将变成事务代理Bean,从而为业务逻辑方法增加事务逻辑。
class="org.springframework.orm.hibernate3.
HibernateTransactionManager">
class="org.springframework.transaction.interceptor.
TransactionInterceptor">
...
一旦增加了如上的配置片段,系统中的业务逻辑方法就有了事务逻辑。这种声明式事务配置方式可以在不同的事务策略之间自由切换。
提示:尽量使用声明式事务配置方式,而不要在代码中完成事务逻辑。
posted @ 2009-07-19 10:24 jadmin 阅读(1) 评论(0) 编辑
时至今日,可能极少有J2EE应用会直接以JDBC方式进行持久层访问。毕竟,用面向对象的程序设计语言来访问关系型数据库,是一件让人沮丧的事情。大部分时候,J2EE应用都会以ORM框架来进行持久层访问,在所有的ORM框架中,Hibernate以其灵巧、轻便的封装赢得了众多开发者的青睐。
Spring具有良好的开放性,能与大部分ORM框架良好整合。下面将详细介绍Spring与Hibernate的整合。
DAO模式是一种标准的J2EE设计模式,DAO模式的核心思想是,所有的数据库访 问,都通过DAO组件完成,DAO组件封装了数据库的增、删、改等原子操作。而业务逻辑组件则依赖于DAO组件提供的数据库原子操作,完成系统业务逻辑的实现。
对于J2EE应用的架构,有非常多的选择,但不管细节如何变换,J2EE应用都大致可分为如下3层:
● 表现层。
● 业务逻辑层。
● 数据持久层。
轻量级J2EE架构以Spring IoC容器为核心,承上启下。其向上管理来自表现层的Action,向下管理业务逻辑层组件,同时负责管理业务逻辑层所需的DAO对象。各层之间负责传值的是值对象,也就是JavaBean实例。
图6.5精确地描绘了轻量级J2EE架构的大致情形。
DAO组件是整个J2EE应用的持久层访问的重要组件,每个J2EE应用的底层实现都难以离开DAO组件的支持。Spring对实现DAO组件提供了许多工具类,系统的DAO组件可通过继承这些工具类完成,从而可以更加简便地实现DAO组件。
Spring的DAO支持,允许使用相同的方式、不同的数据访问技术,如JDBC、Hibernate或JDO。Spring的DAO在不同的持久层访问技术上提供抽象,应用的持久层访问基于Spring的DAO抽象。因此,应用程序可以在不同的持久层技术之间切换。
Spring提供了一系列的抽象类,这些抽象将被作为应用中DAO实现类的父类。通过继承这些抽象类,Spring简化了DAO的开发步骤,能以一致的方式使用数据库访问技术。不管底层采用JDBC、JDO或Hibernate,应用中都可采用一致的编程模型。
图6.5 轻量级J2EE应用架构
应用的DAO类继承这些抽象类,会大大简化应用的开发。最大的好处是,继承这些抽象类的DAO能以一致的方式访问数据库,意味着应用程序可以在不同的持久层访问技术中切换。
除此之外,Spring提供了一致的异常抽象,将原有的Checked异常转换包装成Runtime异常,因而,编码时无须捕获各种技术中特定的异常。Spring DAO体系中的异常,都继承DataAccessException,而DataAccessException异常是Runtime的,无须显式捕捉。通过DataAccessException的子类包装原始异常信息,从而保证应用程序依然可以捕捉到原始异常信息。
Spring提供了多种数据库访问技术的DAO支持,包括Hibernate、JDO、TopLink、iBatis、OJB等。Spring可以使用相同的访问模式、不同的数据库访问技术。就Hibernate的持久层访问技术而言,Spring提供了如下3个工具类(或接口)来支持DAO组件的实现:
● HibernateDaoSupport。
● HibernateTemplate。
● HibernateCallBack。
前面介绍Hibernate时已经知道,在通过Hibernate进行持久层访问时,Hibernate的SessionFactory是一个非常重要的对象,它是单个数据库映射关系编译后的内存镜像。大部分情况下,一个J2EE应用对应一个数据库,也即对应一个SessionFactory对象。
在纯粹的Hibernate访问中,应用程序需要手动创建SessionFactory实例,可想而知,这不是一个优秀的策略。在实际开发中,希望以一种声明式的方式管理SessionFactory实例,直接以配置文件来管理SessionFactory实例,在示范Struts的PlugIn扩展点时,大致示范了这种方式(请参阅2.12.1节的内容)。
Spring的IoC容器则提供了更好的管理方式,它不仅能以声明式的方式配置Session- Factory实例,也可充分利用IoC容器的作用,为SessionFactory注入数据源引用。
下面是Spring配置文件中配置Hibernate SessionFactory的示范代码:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
MySQLDialect
一旦在Spring的IoC容器中配置了SessionFactory Bean,它将随应用的启动而加载,并可以充分利用IoC容器的功能,将SessionFactory Bean注入任何Bean,比如DAO组件。一旦DAO组件获得了SessionFactory Bean的引用,就可以完成实际的数据库访问。
当然,Spring也支持访问容器数据源。如果需要使用容器数据源,可将数据源Bean修改成如下配置:
可见,以声明式的方式管理SessionFactory实例,可以让应用在不同数据源之间切换。如果应用更换数据库等持久层资源,只需对配置文件进行简单修改即可。
提示:以声明式的方式管理SessionFactory,非常类似于早期将数据库服务的相关信息放在web.xml文件中进行配置。这种方式是为了提供更好的适应性,当持久层服务需要更改时,应用代码无须任何改变。
HibernateTemplate提供持久层访问模板,使用HibernateTemplate无须实现特定接口,它只需要提供一个SessionFactory的引用就可执行持久化操作。SessionFactory对象既可通过构造参数传入,也可通过设值方式传入。HibernateTemplate提供如下3个构造函数:
● HibernateTemplate()。
● HibernateTemplate(org.hibernate.SessionFactory sessionFactory)。
● HibernateTemplate(org.hibernate.SessionFactory sessionFactory, boolean allowCreate)。
第一个构造函数,构造一个默认的HibernateTemplate实例。因此,使用Hibernate- Template实例之前,还必须使用方法setSessionFactory(SessionFactory sessionFactory)来为HibernateTemplate传入SessionFactory的引用。
第二个构造函数,在构造时已经传入SessionFactory引用。
第三个构造函数,其boolean型参数表明,如果当前线程已经存在一个非事务性的Session,是否直接返回此非事务性的Session。
在Web应用中,通常启动时自动加载ApplicationContext,SessionFactory和DAO对象都处在Spring上下文管理下,因此无须在代码中显式设置,可采用依赖注入完成Session- Factory和DAO的解耦,依赖关系通过配置文件来设置,如下所示:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
MySQLDialect
在PersonDao组件中,所有的持久化操作都通过HibernateTemplate实例完成,而HibernateTemplate操作数据库非常简洁,大部分CRUD操作都可通过一行代码解决问题。下面介绍如何通过HibernateTemplate进行持久层访问。
HibernateTemplate提供了非常多的常用方法来完成基本的操作,比如通常的增加、删除、修改、查询等操作,Spring 2.0更增加了对命名SQL查询的支持,也增加了对分页的支持。大部分情况下,使用Hibernate的常规用法,就可完成大多数DAO对象的CRUD操作。下面是HibernateTemplate的常用方法简介:
● void delete(Object entity),删除指定持久化实例。
● deleteAll(Collection entities),删除集合内全部持久化类实例。
● find(String queryString),根据HQL查询字符串来返回实例集合。
● findByNamedQuery(String queryName),根据命名查询返回实例集合。
● get(Class entityClass, Serializable id),根据主键加载特定持久化类的实例。
● save(Object entity),保存新的实例。
● saveOrUpdate(Object entity),根据实例状态,选择保存或者更新。
● update(Object entity),更新实例的状态,要求entity是持久状态。
● setMaxResults(int maxResults),设置分页的大小。
下面是一个完整DAO类的源代码:
public class PersonDaoImpl implements PersonDao
{
//执行持久化操作的HibernateTemplate实例
private HibernateTemplate ht = null;
private SessionFactory sessionFactory;
//该DAO组件持久化操作所需的SessionFactory对象
public void setSessionFactory(SessionFactory sessionFactory)
{
this.sessionFactory = sessionFactory;
}
//用于根据SessionFactory实例返回HibernateTemplate实例的方法
private HibernateTemplate getHibernateTemplate()
{
if (ht == null)
{
ht = new HibernateTemplate(sessionFactory);
}
return ht;
}
/**
* 加载人实例
* @param id 需要加载的Person实例的主键值
* @return 返回加载的Person实例
*/
public Person get(int id)
{
return (Person)getHibernateTemplate().get(Person.class, new
Integer(id));
}
/**
* 保存人实例
* @param person 需要保存的Person实例
*/
public void save(Person person)
{
getHibernateTemplate().save(person);
}
/**
* 修改Person实例
* @param person 需要修改的Person实例
*/
public void update(Person person)
{
getHibernateTemplate().update(person);
}
/**
* 删除Person实例
* @param id 需要删除的Person的id
*/
public void delete(int id)
{
getHibernateTemplate().delete(getHibernateTemplate().get(Person.
class,new Integer(id)));
}
/**
* 删除Person实例
* @param person 需要删除的Person实例
*/
public void delete(Person person)
{
getHibernateTemplate().delete(person);
}
/**
* 根据用户名查找Person
* @param name 用户名
* @return 用户名对应的全部用户
*/
public List findByName(String name)
{
return getHibernateTemplate().find("from Person p where p.name
like ?" , name);
}
/**
* 返回全部的Person实例
* @return 全部的Person实例
*/
public List findAllPerson()
{
return getHibernateTemplate().find("from Person ");
}
}
通过上面实现DAO组件的代码可以看出,通过HibernateTemplate进行持久层访问的代码如此清晰,大部分CRUD操作一行代码即可完成,完全无须Hibernate访问那些繁琐的步骤。而且,一旦DAO组件获得了SessionFactory的引用,即可很轻易地创建HibernateTemplate实例。
提示:HibernateTemplate是Spring众多模板工具类之一,Spring正是通过这种简便地封装,完成了开发中大量需要重复执行的工作。
posted @ 2009-07-19 10:24 jadmin 阅读(4) 评论(0) 编辑
Struts的plug-in配置部分明确指出,Spring的配置文件有两个:applicationContext.xml和action-servlet.xml。其实,完全可以使用一个配置文件。通常,习惯将Action Bean配置在控制器的context内。action-servlet.xml用于配置表现层上下文,其详细配置信息如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
因为每次请求都应该启动新的Action处理用户请求,因此,应将Action的作用域配置成Request。
注意:ActionServlet转发请求时,是根据Bean的name属性,而不是id属性。因此,此处确定的name属性与Struts的action属性相同。
applicationContext.xml只有一个bean配置,即vb bean。其详细配置如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
ValidBeanImpl是一个业务逻辑bean,本示例程序中仅作简单的判断,ValidBeanImpl的源代码如下:
//面向接口编程,实现ValidBean接口
public class ValidBeanImpl implements ValidBean
{
//根据输入的用户名和密码判断是否有效
public boolean valid(String username,String pass)
{
//有效,返回true
if (username.equals("scott") && pass.equals("tiger"))
{
return true;
}
return false;
}
}
注意:上面的业务逻辑组件非常简单,它只是一个示意。如果是真实的应用,业务逻辑组件应该通过DAO组件来实现业务逻辑方法。
应用的业务逻辑控制器,Action则负责调用业务逻辑组件的方法,并根据业务逻辑组件方法的返回值,确定如何响应用户请求。下面是该示例应用控制器的代码:
//业务控制器继承Action
public class LoginAction extends Action
{
//action控制器将调用的业务逻辑组件
private ValidBean vb;
//依赖注入业务逻辑组件的setter方法
public void setVb(ValidBean vb)
{
this.vb = vb;
}
//必须重写该核心方法,该方法actionForm将表单的请求参数封装成值对象
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)throws
Exception
{
//form由ActionServlet转发请求时创建,封装了所有的请求参数
LoginForm loginForm = (LoginForm)form;
//获取username请求参数
String username = loginForm.getUsername();
//获取pass请求参数
String pass = loginForm.getPass();
//下面为服务器端的数据校验
String errMsg = "";
//判断用户名不能为空
if (username == null || username.equals(""))
{
errMsg += "您的用户名丢失或没有输入,请重新输入";
}
//判断密码不能为空
else if(pass == null || pass.equals(""))
{
errMsg += "您的密码丢失或没有输入,请重新输入";
}
//如果用户名和密码不为空,才调用业务逻辑组件
else
{
//vb是业务逻辑组件,由容器注入
if (vb.valid(username,pass))
{
return mapping.findForward("welcome");
}
else
{
errMsg = "您的用户名和密码不匹配";
}
}
//判断是否生成了错误信息
if (errMsg != null && !errMsg.equals(""))
{
//如果有错误信息,将错误信息保存在request里,并跳转到input对应的
forward对象
request.setAttribute("err" , errMsg);
return mapping.findForward("input");
}
else
{
//如果没有错误信息,跳转到welcome对应的forward对象
return mapping.findForward("welcome");
}
}
}
在本应用中,使用了Struts的客户端数据校验,让Action继承ValidatorActionForm即可。ActionForm的代码非常简单,此处不再赘述。
为了完成数据校验,还应该编写数据校验规则文件。在struts-config.xml文件的尾部,另有一个plug-in用来加载校验文件,其中validator-rules.xml文件位于struts压缩包的lib下,直接复制过来即可使用,而validator.xml必须自己编写,validator.xml文件如下:
"-//Apache Software Foundation//DTD Commons Validator Rules
Configuration 1.1.3//EN"
"http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">
上面示例程序的结构非常清晰:表现层组件(Action)配置在action-servlet.xml文件中,而业务逻辑层组件(vb)配置在applicationContext.xml文件中,如果应用中有DAO组件,将DAO组件配置在dao-context.xml文件中。将3个文件放在plug-in元素里一起加载。
DelegatingRequestProcessor会将请求转发到Action,该Action已经处于IoC容器管理之下,因此,可以方便地访问容器中的其他Bean。
通过配置文件可以看出,Action根本无须type属性,即struts-config.xml中Action根本没有实例化过,DelegatingRequestProcessor将请求转发给Spring容器中的同名Bean。这种转发的时机非常早,避免了创建struts-config.xml配置文件中的Action,因而性能非常好。
图6.3是采用这种整合策略的执行效果。
使用DelegatingRequestProcessor简单方便,但有一个缺点,RequestProcessor是Struts的一个扩展点,也许应用程序本身就需要扩展RequestProcessor,而DelegatingRequest- Processor已经使用了这个扩展点。
为了重新利用Struts的RequestProcessor这个扩展点,有两个做法:
● 应用程序的RequestProcessor不再继承Struts的RequestProcessor,改为继承DelegatingRequestProcessor。
● 使用DelegatingActionProxy。
前者常常有一些未知的风险,而后者是Spring推荐的整合策略。使用Delegating- ActionProxy与DelegatingRequestProcessor的目的都只有一个,将请求转发给Spring管理的Bean。
DelegatingRequestProcessor直接替换了原有的RequestProcessor,在请求转发给action之前,转发给Spring管理的Bean;而DelegatingActionProxy则被配置成Struts的Action,即所有的请求先被ActionServlet截获,请求被转发到对应的Action,而action的实现类全都是DelegatingActionProxy,DelegatingActionProxy再将请求转发给Spring容器的Action。
可以看出,使用DelegatingActionProxy比使用DelegatingRequestProcessor要晚一步转发到Spring的context。但通过这种方式可以避免占用扩展点。
与使用DelegatingRequestProcessor相比,使用DelegatingActionProxy仅需要去掉controller配置元素,并将所有的action实现类改为DelegatingActionProxy即可。详细的配置文件如下:
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"
"http://struts.apache.org/dtds/struts-config_1_2.dtd">
name="loginForm" scope="request" validate="true" input=
"/login.jsp" >
value="/WEB-INF/applicationContext.xml, /WEB-INF/action-servlet.xml"/>
DelegatingActionProxy接收ActionServlet转发过来的请求,然后转发给Application- Context管理的Bean,这是典型的链式处理。
通过配置文件可以看出,struts-config.xml文件中配置了大量DelegatingActionProxy实例,Spring容器中也配置了同名的Action。即Struts的业务控制器分成了两个部分:第一个部分是Spring的DelegatingActionProxy,这个部分没有实际意义,仅仅完成转发;第二个部分是用户的Action实现类,该实现类负责真实的处理。
这种策略的性能比前一种策略的效果要差一些,因为需要多创建一个Delegating- ActionProxy实例。而且,J2EE应用中Action非常多,这将导致大量创建DelegatingActionProxy实例,使用一次之后,等待垃圾回收机制回收——这对性能的影响不可避免。
图6.4是DelegatingActionProxy的执行效果。
注意:使用DelegatingActionProxy的整合策略,可避免占用Struts的RequestProcessor扩展点,但降低了整合性能。
前面已经介绍了,Spring与Struts的整合还有一种策略,让Struts的Action显式获取Spring容器中的Bean。在这种策略下,Struts的Action不接受IoC容器管理,Action的代码与Spring API部分耦合,造成代码污染。这种策略也有其好处:代码的可读性非常强,Action的代码中显式调用业务逻辑组件,而无须等待容器注入。
Action中访问ApplicationContext有两种方法:
● 利用WebApplicationContextUtils工具类。
● 利用ActionSupport支持类。
通过WebApplicationContextUtils,可以显式获得Spring容器的引用(请参阅6.4.1节的内容),而ActionSupport类则提供了一个更简单的方法getWebApplicationContext(),该方法可直接获取Spring容器的引用。
所谓ActionSupport类,是指Spring提供了系列扩展。Spring扩展了Struts的Action,在Struts的Action后加上Support后缀,Spring扩展的Action有如下几个:
● ActionSupport。
● DispatchActionSupport。
● LookupDispatchActionSupport。
● MappingDispatchActionSupport。
下面的示例将展示这种整合策略,在这种整合策略下,Struts的Action改为继承Spring扩展后的Action,下面是应用的Action代码:
//新的业务控制器,继承Spring的ActionSupport类
public class LoginAction extends ActionSupport
{
//依然将ValidBean作为成员变量
private ValidBean vb;
//构造器,注意:不可在构造器中调用getWebApplicationContext()方法
public LoginAction()
{
}
//完成ValidBean的初始化
public ValidBean getVb()
{
return(ValidBean)getWebApplicationContext().getBean("vb");
}
//必须重写该核心方法,该方法actionForm将表单的请求参数封装成值对象
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)throws
Exception
{
//form由ActionServlet转发请求时创建,封装了所有的请求参数
LoginForm loginForm = (LoginForm)form;
//获取username请求参数
String username = loginForm.getUsername();
//获取pass请求参数
String pass = loginForm.getPass();
//下面为服务器端的数据校验
String errMsg = "";
//判断用户名不能为空
if (username == null || username.equals(""))
{
errMsg += "您的用户名丢失或没有输入,请重新输入";
}
//判断密码不能为空
else if(pass == null || pass.equals(""))
{
errMsg += "您的密码丢失或没有输入,请重新输入";
}
//如果用户名和密码不为空,才调用业务逻辑组件
else
{
//vb是业务逻辑组件,通过上面的初始化方法获得
if (getVb().valid(username,pass))
{
return mapping.findForward("welcome");
}
else
{
errMsg = "您的用户名和密码不匹配";
}
}
//判断是否生成了错误信息
if (errMsg != null && !errMsg.equals(""))
{
//如果有错误信息,将错误信息保存在request里,并跳转到input对应的
//forward对象
request.setAttribute("err" , errMsg);
return mapping.findForward("input");
}
else
{
//如果没有错误信息,跳转到welcome对应的forward对象
return mapping.findForward("welcome");
}
}
}
在上面的Action代码中,Action显式获取容器中的业务逻辑组件,而不是依靠Spring容器的依赖注入。在这种整合策略下,表现层的控制器组件不再接受IoC容器管理。因此,没有控制器上下文,应将原有的action-servlet.xml文件删除,并修改plug-in元素,不要加载该文件。还要修改Action配置,将Action配置的type元素修改成实际的处理类。这 种整合策略也有一个好处:代码的可读性更强,对传统Struts应用开发的改变很小,容易使用。
将该Action部署在struts-config.xml中,Struts将负责创建该Action。struts-config.xml文件的源代码如下:
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"
"http://struts.apache.org/dtds/struts-config_1_2.dtd">
name="loginForm" scope="request" validate="true" input=
"/login.jsp" >
此时,Spring无须使用配置Action的配置文件,这种配置方式非常简单。只需要业务逻辑组件的配置文件,业务逻辑组件的配置文件与前面的示例没有任何改变。
该配置文件中的业务逻辑组件由Spring容器负责实现,而ActionSupport能够先定位Spring容器,然后获得容器的业务逻辑组件。
这种整合策略的执行效果与前面两种整合策略的执行效果完全相同。从代码中分析可见,在这种整合策略下,业务控制器再次退回到Struts起初的设计。仅由strutsconfig.xml中Action充当,从而避免了像DelegatingActionProxy整合策略的性能低下,因为可以只需要创建实际的Action实例。
注意:在这种整合策略下,Struts开发者的改变最小,最接近传统Struts应用开发者的习惯。但这种整合策略会造成代码污染,因为Action类必须继承Spring的ActionSupport类。
posted @ 2009-07-19 10:23 jadmin 阅读(1) 评论(0) 编辑
虽然Spring也提供了自己的MVC组件,但一来Spring的MVC组件过于繁琐,二 来Struts的拥护者实在太多。因此,很多项目都会选择使用Spring整合Struts框架。而且Spring确实可以无缝整合Struts框架,二者结合成一个更实际的J2EE开发平台。
使用Spring的Web应用时,不用手动创建Spring容器,而是通过配置文件声明式地创建Spring容器。因此,在Web应用中创建Spring容器有如下两个方式:
● 直接在web.xml文件中配置创建Spring容器。
● 利用第三方MVC框架的扩展点,创建Spring容器。
其实第一种创建Spring容器的方式更加常见。为了让Spring容器随Web应用的启动而自动启动,有如下两个方法:
● 利用ServletContextListener实现。
● 采用load-on-startup Servlet实现。
Spring提供ServletContextListener的一个实现类ContextLoaderListener,该类可以作为Listener使用,会在创建时自动查找WEB-INF/下的applicationContext.xml文件,因此,如果只有一个配置文件,并且文件名为applicationContext.xml,只需在web.xml文件中增加如下配置片段即可:
ContextLoaderListener
如果有多个配置文件需要载入,则考虑使用
带多个配置文件的web.xml文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee version="2.4">
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
applicationContext.xml
ContextLoaderListener
如果没有通过contextConfigLocation指定配置文件,Spring会自动查找application- Context.xml配置文件;如果有contextConfigLocation,则利用该参数确定的配置文件。如果无法找到合适的配置文件,Spring将无法正常初始化。
Spring根据bean定义创建WebApplicationContext对象,并将其保存在web应用的ServletContext中。大部分情况下,应用中的Bean无须感受到ApplicationContext的存在,只要利用ApplicationContext的IoC即可。
如果需要在应用中获取ApplicationContext实例,可以通过如下代码获取:
//获取当前Web应用的Spring容器
WebApplicationContext ctx =
WebApplicationContextUtils.getWebApplicationContext(servletContext);
除此之外,Spring提供了一个特殊的Servlet类ContextLoaderServlet。该Servlet在启动时,会自动查找WEB-INF/下的applicationContext.xml文件。
当然,为了让ContextLoaderServlet随应用的启动而启动,应将此Servlet配置成load-on-startup的Servlet,load-on-startup的值小一点比较合适,这样可以保证Application- Context更快的初始化。
如果只有一个配置文件,并且文件名为applicationContext.xml,在web.xml文件中增加如下一段即可:
该Servlet用于提供“后台”服务,主要用于创建Spring容器,无须响应客户请求,因此无须配置servlet-mapping。
如果有多个配置文件,一样使用
事实上,不管是ContextLoaderServlet,还是ContextLoaderListener,都依赖于ContextLoader创建ApplicationContext实例。
在ContextLoader代码的第240行,有如下代码:
String configLocation = servletContext.getInitParameter
(CONFIG_LOCATION_PARAM);
if (configLocation != null) {
wac.setConfigLocations(StringUtils.tokenizeToStringArray
(configLocation,
ConfigurableWebApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
其中,CONFIG_LOCATION_PARAM是该类的常量,其值为contextConfigLocation。可以看出,ContextLoader首先检查servletContext中是否有contextConfigLocation的参数,如果有该参数,则加载该参数指定的配置文件。
ContextLoaderServlet与ContextLoaderListener底层都依赖于ContextLoader。因此,二者的效果几乎没有区别。之间的区别不是它们本身引起的,而是由于Servlet规范,Listener比Servlet优先加载。因此,采用ContextLoaderListener创建ApplicationContext的时机更早。
当然,也可以通过ServletContext的getAttribute方法获取ApplicationContext。但使用WebApplicationContextUtils类更便捷,因为无须记住ApplicationContext的属性名。即使ServletContext的WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRI- BUTE属性没有对应对象,WebApplicationContextUtils的getWebApplicationContext()方法将会返回空,而不会引起异常。
到底需要使用Listener,还是使用load-on-startup Servlet来创建Spring容器呢?通常推荐使用Listener来创建Spring容器。但Listerner是Servlet 2.3以上才支持的标准,因此,必须Web容器支持Listener才可使用Listerner。
注意:使用Listener创建Spring容器之前,应先评估Web容器是否支持Listener标准。
还有一种情况,利用第三方MVC框架的扩展点来创建Spring容器,比如Struts。在第2章介绍Strust框架时,知道Struts有一个扩展点PlugIn。
实际上,Spring正是利用了PlugIn这个扩展点,从而提供与Struts的整合。Spring提供了PlugIn接口的实现类org.springframework.web.struts.ContextLoaderPlugIn。这个实现类可作为Struts的PlugIn配置,Struts框架启动时,将自动创建Spring容器。
为了利用Struts的PlugIn创建Spring容器,只需在Struts配置文件中增加如下片段 即可:
value="/WEB-INF/action-servlet.xml,/WEB-INF/applicationContext. 其中,指定contextConfigLocation属性值时,即可以指定一个Spring配置文件的位置,可以指定多个Spring配置文件的位置。 对于一个基于B/S架构的J2EE应用而言,用户请求总是向MVC框架的控制器请求,而当控制器拦截到用户请求后,必须调用业务逻辑组件来处理用户请求。此时有一个问题,控制器应该如何获得业务逻辑组件? 最容易想到的策略是,直接通过new关键字创建业务逻辑组件,然后调用业务逻辑组件的方法,根据业务逻辑方法的返回值确定结果。 实际的应用中,很少见到采用上面的访问策略,因为这是一种非常差的策略。不这样做至少有如下3个原因: ● 控制器直接创建业务逻辑组件,导致控制器和业务逻辑组件的耦合降低到代码层次,不利于高层次解耦。 ● 控制器不应该负责业务逻辑组件的创建,控制器只是业务逻辑组件的使用者。无须关心业务逻辑组件的实现。 ● 每次创建新的业务逻辑组件将导致性能下降。 答案是采用工厂模式或服务定位器。采用服务定位器的模式,是远程访问的场景。在这种场景下,业务逻辑组件已经在某个容器中运行,并对外提供某种服务。控制器无须理会该业务逻辑组件的创建,直接调用即可,但在调用之前,必须先找到该服务——这就是服务定位器的概念。经典J2EE应用就是这种结构的应用。 对于轻量级的J2EE应用,工厂模式则是更实际的策略。因为轻量级的J2EE应用里,业务逻辑组件不是EJB,通常就是一个POJO,业务逻辑组件的生成通常由工厂负责,而且工厂可以保证该组件的实例只需一个就够了,可以避免重复实例化造成的系统开销。 如图6.2就是采用工厂模式的顺序图。 图6.2 工厂模式顺序图 采用工厂模式,将控制器与业务逻辑组件的实现分离,从而提供更好的解耦。 在采用工厂模式的访问策略中,所有的业务逻辑组件的创建由工厂负责,业务逻辑组件的运行也由工厂负责。而控制器只需定位工厂实例即可。 如果系统采用Spring框架,则Spring成为最大的工厂。Spring负责业务逻辑组件的创建和生成,并可管理业务逻辑组件的生命周期。可以如此理解,Spring是一个性能非常优秀的工厂,可以生产出所有的实例,从业务逻辑组件,到持久层组件,甚至控制器。 现在的问题是,控制器如何访问到Spring容器中的业务逻辑组件?为了让Action访 问Spring的业务逻辑组件,有两种策略: ● Spring管理控制器,并利用依赖注入为控制器注入业务逻辑组件。 ● 控制器显式定位Spring工厂,也就是Spring的容器ApplicationContext实例,并从工厂中获取业务逻辑组件实例的引用。 第一种策略,充分利用Spring的IoC特性,是最优秀的解耦策略。但不可避免带来一些不足之处,归纳起来主要有如下不足之处: ● Spring管理Action,必须将所有的Action配置在Spring容器中,而struts-config.xml文件中的配置也不会减少,导致配置文件大量增加。 ● Action的业务逻辑组件接收容器注入,将导致代码的可读性降低。 总体而言,这种整合策略是利大于弊。 第二种策略,与前面介绍的工厂模式并没有太大的不同。区别是Spring容器充当了业务逻辑组件的工厂。控制器负责定位Spring容器,通常Spring容器访问容器中的业务逻辑组件。这种策略是一种折衷,降低了解耦,但提高了程序的可读性。 Spring完全支持这两种策略,既可以让Spring容器管理控制器,也可以让控制器显式定位Spring容器中的业务逻辑组件。 这里介绍的是第一种整合策略:让Spring管理Struts的Action。那么同样有一个问题,让Spring管理Struts的Action时,客户端的HTTP 请求如何转向Spring容器中的Action? 当使用Struts作为MVC框架时,客户端的HTTP请求都是直接向ActionServlet请求的,因此关键就是让ActionServlet将请求转发给Spring容器中的Action。这很明显可以利用Spring的另一个扩展点:通过扩展RequestProcessor完成,使用扩展的RequestProcessor替换Struts的RequestProcessor。 Spring完成了这种扩展,Spring提供的DelegatingRequestProcessor继承Request- Processor。为了让Struts使用DelegatingRequestProcessor,还需要在struts-config.xml文件中增加如下一行: //使用spring的RequestProcessor替换struts原有的RequestProcessor 完成这个设置后,Struts会将截获到的用户请求转发到Spring context下的bean,根据bean的name属性来匹配。而Struts中的action配置则无须配置class属性,即使配置了class属性也没有任何用处,即下面两行配置是完全一样的: //配置struts action时,指定了实现类 //配置struts action时,没有指定实现类 下面的示例程序在上一个示例程序的基础上稍作修改,增加了客户端验证和程序国际化部分。也调用了Spring的业务bean来验证登录。先看修改后的struts-config.xml文件:
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN" "http://struts.apache.org/dtds/struts-config_1_2.dtd"> scope="request" validate="true" input="/login.jsp" > value="/WEB-INF/applicationContext.xml, /WEB-INF/action-servlet.xml"/> 修改后的struts-config.xml文件,增加加载国际化资源文件。配置Struts的action不需要class属性,完成了ApplicationContext的创建。 然后考虑web.xml文件的配置,在web.xml文件中必须配置Struts框架的加载。除此之外,因为使用了Spring管理Struts的Action,而Action是随HTTP请求启动的,因此,应将Action的作用域配置成Request,为了使用Request作用域,必须在web.xml文件中增加适当的配置。 下面是web.xml文件的代码: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee version="2.4"> posted @ 2009-07-19 10:22 jadmin 阅读(2) 评论(0) 编辑
xml"/>6.4.2 MVC框架与Spring整合的思考
6.4.3 使用DelegatingRequestProcessor
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
RequestContextFilter
虽然在上面的配置片段中,仅仅配置了JDBC局部事务管理器、Hibernate局部事务管理器、JDBC全局事务管理器等。但Spring支持大部分持久化策略的事务管理器。
不论采用何种持久化策略,Spring都提供了一致的事务抽象,因此,应用开发者能在任何环境下,使用一致的编程模型。无须更改代码,应用就可在不同的事务管理策略中切换。Spring同时支持声明式事务管理和编程式事务管理。
使用编程式事务管理,开发者使用的是Spring事务抽象,而无须使用任何具体的底层事务API。Spring的事务管理将代码从底层具体的事务API中抽象出来,该抽象可以使用在任何底层事务基础之上。
使用声明式策略,开发者通常书写很少的事务管理代码,因此,不依赖Spring或任何其他事务API。Spring的声明式事务无须任何额外的容器支持,Spring容器本身管理声明式事务。使用声明事务策略,无须在业务代码中书写任何事务代码,可以让开发者更好地专注于业务逻辑的实现。Spring管理的事务支持多个事务资源的跨越,但无法支持跨越远程调用的事务上下文传播。
Spring同时支持编程式事务策略和声明式事务策略,大部分时候,都推荐采用声明式事务策略,使用声明事务策略的优势十分明显:
● 声明式事务能大大降低开发者的代码书写量。而且声明式事务几乎不需要影响应用的代码。因此,无论底层事务策略如何变化,应用程序无须任何改变。
● 应用程序代码无须任何事务处理代码,可以更专注于业务逻辑的实现。
● Spring则可对任何POJO的方法提供事务管理,而且Spring的声明式事务管理无须容器的支持,可在任何环境下使用。
● EJB的CMT无法提供声明式回滚规则。而通过配置文件,Spring可指定事务在遇到特定异常时自动回滚。Spring不仅可在代码中使用setRollbackOnly回滚事务,也可在配置文件中配置回滚规则。
● 由于Spring采用AOP的方式管理事务,因此,可以在事务回滚动作中插入用户自己的动作,而不仅仅是执行系统默认的回滚。
提示:本节不打算全面介绍Spring的各种事务策略,因此本节不会介绍编程式事务。如果读者需要更全面了解Spring事务的相关方面,请参阅笔者所著的《Spring2.0宝典》 一书。
对于采用声明式事务策略,可以使用TransactionProxyFactoryBean来配置事务代理Bean。正如它的类名所暗示的,它是一个工厂Bean,工厂Bean用于生成一系列的Bean实例,这一系列的Bean实例都是Proxy。
可能读者已经想到了,既然TransactionProxyFactoryBean产生的是代理Bean,可见这种事务代理正是基于Spring AOP组件的。配置TransactionProxyFactoryBean时,一样需要指定目标Bean。
每个TransactionProxyFactoryBean为一个目标Bean生成事务代理,事务代理的方法改写了目标Bean的方法,就是在目标Bean的方法执行之前加入开始事务,在目标Bean的方法正常结束之前提交事务,如果遇到特定异常则回滚事务。
TransactionProxyFactoryBean创建事务代理时,需要了解当前事务所处的环境,该环境属性通过PlatformTransactionManager实例传入,而相关事务传入规则在TransactionProxy- FactoryBean的定义中给出。
下面给出声明式事务配置文件的完整代码:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.orm.hibernate3. class="org.springframework.transaction.interceptor.
MySQLDialect
HibernateTransactionManager">
TransactionProxyFactoryBean">
-MyCheckedException
在上面的定义文件中,没有对DAO对象采用Service层包装。通常情况下,DAO层上应有一层Service层。事务代理则以Service层Bean为目标Bean。此处为了简化配置,TransactionProxyFactoryBean直接以DAO bean作为目标bean,这一点不会影响事务代理的生成。
事务回滚规则部分定义了三个回滚规则:
第一个回滚规则表示所有以insert开始的方法,都应该满足该事务规则。PROPAGATION_REQUIRED事务传播规则指明,该方法必须处于事务环境中,如果当前执行线程已处于事务环境下,则直接执行;否则,启动新的事务然后执行该方法。该规则还指定,如果方法抛出MyCheckedException的实例及其子类的实例,则强制回滚。MyCheckedException前的“-”表示强制回滚;“+”则表示强制提交,即某些情况下,即使抛出异常也强制提交;
第二个回滚规则表示所有以update开头的方法,都遵守PROPAGATION_REQUIRED的事务传播规则;
第三个回滚规则表示除前面规定的方法外,其他所有方法都采用PROPAGATION_ REQUIRED事务传播规则,而且只读。
常见的事务传播规则有如下几个:
● PROPAGATION_MANDATORY,要求调用该方法的线程必须处于事务环境中,否则抛出异常。
● PROPAGATION_NESTED,如果执行该方法的线程已处于事务环境下,依然启动新的事务,方法在嵌套的事务里执行。如果执行该方法的线程序并未处于事务中,也启动新的事务,然后执行该方法,此时与PROPAGATION_REQUIRED相同。
● PROPAGATION_NEVER,不允许调用该方法的线程处于事务环境下,如果调用该方法的线程处于事务环境下,则抛出异常。
● PROPAGATION_NOT_SUPPORTED,如果调用该方法的线程处在事务中,则先暂停当前事务,然后执行该方法。
● PROPAGATION_REQUIRED,要求在事务环境中执行该方法,如果当前执行线程已处于事务中,则直接调用;如果当前执行线程不处于事务中,则启动新的事务后执行该方法。
● PROPAGATION_REQUIRES_NEW,该方法要求有一个线程在新的事务环境中执行,如果当前执行线程已处于事务中,先暂停当前事务,启动新的事务后执行该方法;如果当前调用线程不处于事务中,则启动新的事务后执行该方法。
● PROPAGATION_SUPPORTS,如果当前执行线程处于事务中,则使用当前事务,否则不使用事务。
程序里原来使用personDAO的地方,无须变化。因为,配置文件里将personDAO目标Bean的id改成personDAOTarget,为TransactionProxyFactoryBean工厂Bean所产生的代理Bean命名为personDAO。该代理Bean会包含原有personDAO的所有方法,而且为这些方法增加了不同的事务处理规则。
程序面向PersonDaoImpl类所实现的接口编程,TransactionProxyFactoryBean生成的代理Bean也会实现TransactionProxyFactoryBean接口。因此,原有的程序中调用DAO组件的代码无须任何改变。程序运行时,由事务代理完成原来目标Bean完成的工作。
事实上,Spring不仅支持对接口的代理,整合CGLIB后,Spring甚至可对具体类生成代理。只要设置proxyTargetClass属性为true就可以。如果目标Bean没有实现任何接口,proxyTargetClass属性默认被设为true,此时Spring会对具体类生成代理。当然,通常建议面向接口编程,而不要面向具体的实现类编程。
仔细观察配置文件中两个事务代理Bean的配置时,发现两个事务代理Bean的大部分配置完全相同,如果配置文件中包含大量这样的事务代理Bean配置,配置文件将非常臃肿。考虑到大部分事务代理Bean的配置都大同小异,可以使用Bean继承来简化事务代理的配置。
正如前面部分介绍的,Bean继承是将所有子Bean中相同的配置定义成一个模板,并将此模板Bean定义成一个抽象Bean。考虑所有事务代理Bean中,有如下部分是大致相 同的:
● 事务代理Bean所使用的事务管理器。
● 事务传播规则。
因此,现在配置文件中定义如下的事务代理模板Bean,其配置代码如下:
class="org.springframework.transaction.interceptor.
TransactionProxyFactoryBean">
而真正的事务代理Bean,则改为继承上面的事务模板Bean。考虑到将目标Bean定义在Spring容器中可能增加未知的风险,因此将目标Bean定义成嵌套Bean。
此时的personDAO Bean无须具体地定义事务属性,它将在其父Bean txProxyTemplate中获取事务定义属性。此处采用嵌套Bean来定义目标Bean,因此,并未将目标Bean直接暴露在Spring的上下文中让其他模块调用。当然,也可采用一个已经存在的Bean作为目标Bean;子Bean的事务属性定义,完全可覆盖事务代理模板里的事务属性定义。如下例所示:
可见,采用Bean继承方式定义事务代理的方式,可以很好地简化事务代理的配置,可以避免配置事务代理Bean时的冗余配置。
提示:使用Bean继承可以很好地简化事务代理Bean的配置,通过将各事务代理Bean共同的配置信息提取成事务模板Bean,可以让实际的事务代理Bean的配置更加简洁;而且,配置方式相当直观。尽量将目标Bean配置成嵌套Bean,这样的方式可以保证更好的内聚性。
如果读者还记得前面介绍的AOP知识,应该知道还有一种更加简洁的配置,就是利用Bean后处理器,让Bean后处理器为容器中其他Bean自动创建事务代理。
回顾6.2.6节和6.2.7节,读者可能已经想到如何自动创建代理。是的,正是通过6.2.6节和6.2.7节所给出的两个自动代理创建类来生成事务代理。
正如前文已经提到的,使用BeanNameAutoProxyCreator和DefaultAdvisorAutoProxy- Creator来创建代理时,并不一定是创建事务代理,关键在于传入的拦截器,如果传入事务拦截器,将可自动生成事务代理。
下面是使用BeanNameAutoProxyCreator自动生成事务代理的配置文件:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.jdbc.datasource.DataSource- class="org.springframework.transaction.interceptor.
TransactionManager">
TransactionInterceptor">
如果配置文件中仅有两个目标Bean,可能不能很清楚地看出这种自动创建代理配置方式的优势,但如果有更多目标Bean需要自动创建事务代理,则可以很好地体会到这种配置方式的优势:配置文件只需要简单地配置目标Bean,然后在BeanNameAutoProxyCreator配置中增加一行即可。
提示:使用BeanNameAutoProxyCreator可以自动创建事务代理,使用DefaultAdvisor- AutoProxyCreator也可自动创建事务代理。关于后一个Bean后处理器的配置方式,请参看前面6.2.7节的内容。
posted @ 2009-07-19 10:18 jadmin 阅读(5) 评论(0) 编辑
Spring的事务管理不需与任何特定的事务API耦合。对不同的持久层访问技术,编程式事务提供一致的事务编程风格,通过模板化的操作一致性地管理事务。声明式事务基于Spring AOP实现,却并不需要程序开发者成为AOP专家,亦可轻易使用Spring的声明式事务管理。
Spring事务策略是通过PlatformTransactionManager接口体现的,该接口是Spring事务策略的核心。该接口的源代码如下:
public interface PlatformTransactionManager
{
//平台无关的获得事务的方法
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
//平台无关的事务提交方法
void commit(TransactionStatus status) throws TransactionException;
//平台无关的事务回滚方法
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager是一个与任何事务策略分离的接口,随着底层不同事务策略切换,应用必须采用不同的实现类。PlatformTransactionManager接口没有与任何事务资源捆绑在一起,它可以适应于任何的事务策略,结合Spring的IoC容器,可以向PlatformTransactionManager注入相关的平台特性。
PlatformTransactionManager接口有许多不同的实现类,应用程序面向与平台无关的接口编程,对不同平台的底层支持,由PlatformTransactionManager接口的实现类完成。从而,应用程序无须与具体的事务API耦合。因此,使用PlatformTransactionManager接口,可将代码从具体的事务API中解耦出来。
即使使用特定容器管理的JTA,代码依然无须执行JNDI查找,无须与特定的JTA资源耦合在一起。通过配置文件,JTA资源传给PlatformTransactionManager的实现类。因此,程序的代码可在JTA事务管理和非JTA事务管理之间轻松切换。
在PlatformTransactionManager接口内,包含一个getTransaction(TransactionDefinition definition)方法,该方法根据一个TransactionDefinition参数,返回一个TransactionStatus对象。TransactionStatus对象表示一个事务。TransactionStatus被关联在当前执行的线程。
getTransaction(TransactionDefinition definition)返回的TransactionStatus对象,可能是一个新的事务,也可能是一个已经存在的事务对象。如果当前执行的线程已经处于事务管理下,返回当前线程的事务对象,否则,返回当前线程的调用堆栈已有的事务对象。
TransactionDefinition接口定义了一个事务规则,该接口必须指定如下几个属性值:
● 事务隔离,当前事务和其他事务的隔离程度。例如,这个事务能否看到其他事务未提交的数据等。
● 事务传播,通常,在事务中执行的代码都会在当前事务中运行。但是,如果一个事务上下文已经存在,有几个选项可指定该事务性方法的执行行为。例如,大多数情况下,简单地在现有的事务上下文中运行;或者挂起现有事务,创建一个新的事务。Spring提供EJB CMT(Contain Manager Transaction,容器管理事务)中所有的事务传播选项。
● 事务超时,事务在超时前能运行多久。事务的最长持续时间。如果事务一直没有被提交或回滚,将在超出该时间后,系统自动回滚事务。
● 只读状态,只读事务不修改任何数据。在某些情况下(例如使用Hibernate时),只读事务是非常有用的优化。
TransactionStatus代表事务本身,它提供了简单的控制事务执行和查询事务状态的方法。这些方法在所有的事务API中都是相同的。TransactionStatus接口的源代码如下:
public interface TransactionStatus
{
//判断事务是否是新建的事务
boolean isNewTransaction();
//设置事务回滚
void setRollbackOnly();
//查询事务是否已有回滚标志
boolean isRollbackOnly();
}
Spring的事务管理由PlatformTransactionManager的不同实现类完成。在Spring上下文中配置PlatformTransactionManager Bean时,必须针对不同环境提供不同的实现类。
下面提供不同的持久层访问环境,及其对应的PlatformTransactionManager实现类的 配置。
JDBC数据源的局部事务策略:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.jdbc.datasource.
DataSourceTransactionManager">
对于容器管理JTA数据源,全局事务策略的配置文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.transaction.jta.
JtaTransactionManager" />
对于采用Hibernate持久层访问策略时,局部事务策略的配置文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.orm.hibernate3.
MySQLDialect
HibernateTransactionManager">
对于采用Hibernate持久层访问策略时,全局事务策略的配置文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.transaction.jta.
MySQLDialect
JtaTransactionManager" />
不论采用哪种持久层访问技术,只要使用JTA数据源,Spring事务管理器的配置都是一样的,因为它们都采用的是全局事务管理。
可以看到,仅仅通过配置文件的修改,就可以在不同的事务管理策略间切换,即使从局部事务到全局事务的切换。
提示:Spring所支持的事务策略非常灵活,Spring的事务策略允许应用程序在不同事务策略之间自由切换,即使需要在局部事务策略和全局事务策略之间切换,只需要修改配置文件,而应用程序的代码无须任何改变。这种灵活的设计,又何尝不是因为面向接口编程带来的优势,可见面向接口编程给应用程序更好的适应性。
posted @ 2009-07-19 10:18 jadmin 阅读(2) 评论(0) 编辑
当目标Bean的实现类实现了接口后,Spring AOP可以为其创建JDK动态代理,而无须使用CGLIB创建的代理,这种代理称为代理接口。
创建AOP代理必须指定两个属性:目标Bean和处理。实际上,很多AOP框架都以拦截器作为处理。因为Spring AOP与IoC容器的良好整合,因此配置代理Bean时,完全可以利用依赖注入来管理目标Bean和拦截器Bean。
下面的示例演示了基于AOP的权限认证,它是简单的TestService接口,该接口模拟Service组件,该组件内包含两个方法:
● 查看数据。
● 修改数据。
接口的源代码如下:
//Service组件接口
public interface TestService
{
//查看数据
void view();
//修改数据
void modify();
}
该接口的实现类实现两个方法。因为篇幅限制,本示例并未显示出完整的查看数据和修改数据的持久层操作,仅仅在控制台打印两行信息。实际的项目实现中,两个方法的实现则改成对持久层组件的调用,这不会影响示例程序的效果。实现类的源代码如下:
TestService接口的实现类
public class TestServiceImpl implements TestService
{
//实现接口必须实现的方法
public void view()
{
System.out.println("用户查看数据");
}
//实现接口必须实现的方法
public void modify()
{
System.out.println("用户修改数据");
}
}
示例程序采用Around 处理作为拦截器,拦截器中使用依赖注入获得当前用户名。实际Web应用中,用户应该从session中读取。这不会影响示例代码的效果。拦截器源代码如下:
public class AuthorityInterceptor implements MethodInterceptor
{
//当前用户名
private String user;
//依赖注入所必需的setter方法
public void setUser(String user)
{
this.user = user;
}
public Object invoke(MethodInvocation invocation) throws Throwable
{
//获取当前拦截的方法名
String methodName = invocation.getMethod().getName();
//下面执行权限检查
//对既不是管理员,也不是注册用户的情况
if (!user.equals("admin") && !user.equals("registedUser"))
{
System.out.println("您无权执行该方法");
return null;
}
//对仅仅是注册用户,调用修改数据的情况
else if (user.equals("registedUser") && methodName.equals
("modify"))
{
System.out.println("您不是管理员,无法修改数据");
return null;
}
//对管理员或注册用户,查看数据的情况
else
{
return invocation.proceed();
}
}
}
TestAction类依赖TestService。因篇幅关系,此处不给出TestAction的接口的源代码,TestActionImpl的源代码如下:
public class TestActionImpl
{
//将TestService作为成员变量,面向接口编程
private TestService ts;
//依赖注入的setter方法
public void setTs(TestService ts)
{
this.ts = ts;
}
//修改数据
public void modify()
{
ts.modify();
}
//查看数据
public void view()
{
ts.view();
}
}
配置文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
主程序请求testAction Bean,然后调用该Bean的两个方法,主程序如下:
public class BeanTest
{
public static void main(String[] args)throws Exception
{
//创建Spring容器实例
ApplicationContext ctx = new FileSystemXmlApplicationContext
("bean.xml");
//获得TestAction bean
TestAction ta = (TestAction)ctx.getBean("testAction");
//调用bean的两个测试方法
ta.view();
ta.modify();
}
}
程序执行结果如下:
[java] 用户查看数据
[java] 用户修改数据
代理似乎没有发挥任何作用。因为配置文件中的当前用户是admin,admin用户具备访问和修改数据的权限,因此代理并未阻止访问。将配置文件中的admin修改成registed- User,再次执行程序,得到如下结果:
[java] 用户查看数据
[java] 您不是管理员,无法修改数据
代理阻止了registedUser修改数据,查看数据可以执行。将registedUser修改成其他用户,执行程序,看到如下结果:
[java] 您无权执行该方法
[java] 您无权执行该方法
代理阻止用户对两个方法的执行。基于AOP的权限检查,可以降低程序的代码量,因为无须每次调用方法之前,手动编写权限检查代码;同时,权限检查与业务逻辑分离,提高了程序的解耦。
示例中的目标Bean被暴露在容器中,可以被客户端代码直接访问。为了避免客户端代码直接访问目标Bean,可以将目标Bean定义成代理工厂的嵌套Bean,修改后的配置文件如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
由上面介绍的内容可见,Spring的AOP是对JDK动态代理模式的深化。通过Spring AOP组件,允许通过配置文件管理目标Bean和AOP所需的处理。
下面将继续介绍如何为没有实现接口的目标Bean创建CGLIB代理。
如果目标类没有实现接口,则无法创建JDK动态代理,只能创建CGLIB代理。如果需要没有实现接口的Bean实例生成代理,配置文件中应该修改如下两项:
● 去掉
● 增加
注意:最好面向接口编程,不要面向类编程。同时,即使在实现接口的情况下,也可强制使用CGLIB代理。
CGLIB代理在运行期间产生目标对象的子类,该子类通过装饰器设计模式加入到Advice中。因为CGLIB代理是目标对象的子类,则必须考虑保证如下两点:
● 目标类不能声明成final,因为final类不能被继承,无法生成代理。
● 目标方法也不能声明成final,final方法不能被重写,无法得到处理。
当然,为了需要使用CGLIB代理,应用中应添加CGLIB二进制Jar文件。
这是一种自动创建事务代理的方式,一旦在容器中配置了BeanNameAutoProxyCreator实例,该实例将会对指定名字的Bean实例自动创建代理。实际上,BeanNameAutoProxyCreator是一个Bean后处理器,理论上它会对容器中所有的Bean进行处理,实际上它只对指定名字的Bean实例创建代理。
BeanNameAutoProxyCreator根据名字自动生成事务代理,名字匹配支持通配符。
与ProxyFactoryBean一样,BeanNameAutoProxyCreator需要一个interceptorNames属性,该属性名虽然是“拦截器”,但并不需要指定拦截器列表,它可以是Advisor或任何处理类型。
下面是使用BeanNameAutoProxyCreator的配置片段:
class="org.springframework.transaction.interceptor.
TransactionInterceptor">
上面的片段是使用BeanNameAutoProxyCreator自动创建事务代理的片段。Transaction- Interceptor用来定义事务拦截器,定义事务拦截器时传入事务管理器Bean,也指定事务传播属性。
通过BeanNameAutoProxyCreator定义Bean后处理器,定义该Bean后处理器时,通过beanNames属性指定有哪些目标Bean生成事务代理;还需要指定“拦截器链”,该拦截器链可以由任何处理组成。
定义目标Bean还可使用通配符,使用通配符的配置片段如下所示:
上面的配置片段中,所有名字以DAO、Service、Manager结尾的bean,将由该“bean后处理器”为其创建事务代理。目标bean不再存在,取而代之的是目标bean的事务代理。通过这种方式,不仅可以极大地降低配置文件的繁琐,而且可以避免客户端代码直接调用目标bean。
注意:虽然上面的配置片段是为目标对象自动生成事务代理。但这不是唯一的,如果有需要,可以为目标对象生成任何的代理。BeanNameAutoProxyCreator为目标对象生成怎样的代理,取决于传入怎样的处理Bean,如果传入事务拦截器,则生成事务代理Bean;否则将生成其他代理,在后面的实例部分,读者将可以看到大量使用BeanNameAutoProxy- Creator创建的权限检查代理。
Spring还提供了另一个Bean后处理器,它也可为容器中的Bean自动创建代理。相比之下,DefaultAdvisorAutoProxyCreator是更通用、更强大的自动代理生成器。它将自动应用于当前容器中的advisor,不需要在DefaultAdvisorAutoProxyCreator定义中指定目标Bean的名字字符串。
这种定义方式有助于配置的一致性,避免在自动代理创建器中重复配置目标Bean 名。
使用该机制包括:
● 配置DefaultAdvisorAutoProxyCreator bean定义。
● 配置任何数目的Advisor,必须是Advisor,不仅仅是拦截器或其他处理。因为,必须使用切入点检查处理是否符合候选Bean定义。
DefaultAdvisorAutoProxyCreator计算Advisor包含的切入点,检查处理是否应该被应用到业务对象,这意味着任何数目的Advisor都可自动应用到业务对象。如果Advisor中没有切入点符合业务对象的方法,这个对象就不会被代理。如果增加了新的业务对象,只要它们符合切入点定义,DefaultAdvisorAutoProxyCreator将自动为其生成代理,无须额外 配置。
当有大量的业务对象需要采用相同处理,DefaultAdvisorAutoProxyCreator是非常有用的。一旦定义恰当,直接增加业务对象,而不需要额外的代理配置,系统自动为其增加 代理。
class="org.springframework.orm.hibernate3.
HibernateTransactionManager">
class="org.springframework.transaction.interceptor.
TransactionInterceptor">
本应用的chinese具有两个初始化方法:
● init-method指定初始化方法。
● 实现InitializingBean接口,提供了afterPropertiesSet初始化方法。
MyBeanPostProcessor类实现了BeanPostProcessor接口,并实现了该接口的两个方法,这两个方法分别在初始化方法调用之前和之后得到回调。
注意:上面的配置文件配置Bean后处理器时,依然为Bean处理器指定了id属性,指定id属性是为了方便程序通过该id属性访问Bean后处理器。大部分时候,程序无须手动访问该Bean后处理器,因此无须为其指定id属性。
主程序如下:
public class BeanTest
{
public static void main(String[] args)throws Exception
{
//CLASSPATH路径下的bean.xml文件创建Resource对象
ClassPathResource isr = new ClassPathResource("bean.xml");
//以Resource对象作为参数,创建BeanFactory的实例
XmlBeanFactory factory = new XmlBeanFactory(isr);
//获取Bean后处理器实例
MyBeanPostProcessor beanProcessor =
(MyBeanPostProcessor)factory.getBean("beanPostProcessor");
//注册BeanPostProcessor实例
factory.addBeanPostProcessor(beanProcessor);
System.out.println("程序已经实例化BeanFactory...");
Person p = (Person)factory.getBean("chinese");
System.out.println("程序中已经完成了chinese bean的实例化...");
p.useAxe();
}
}
如果使用BeanFactory作为Spring容器,必须手动注册Bean后处理器,因此在程序中先获取Bean后处理器实例,然后手动注册——这就是在配置文件中指定Bean后处理器id属性的原因。通过BeanFactory的addBeanPostProcessor可以注册BeanPostProcessor实例。程序执行结果如下:
[java] 程序已经实例化BeanFactory...
[java] Spring实例化主调bean:Chinese实例...
[java] Spring实例化依赖bean:SteelAxe实例...
[java] 系统正在准备对steelAxe进行初始化...
[java] 系统已经完成对steelAxe的初始化
[java] Spring执行依赖关系注入...
[java] 系统正在准备对chinese进行初始化...
[java] 正在执行初始化方法 afterPropertiesSet...
[java] 正在执行初始化方法 init...
[java] 系统已经完成对chinese的初始化
[java] 程序中已经完成了chinese bean的实例化...
[java] wawa钢斧砍柴真快
在配置文件中配置chinese实例时,并未指定name属性值。但程序执行时,name属性有了值,这就是Bean后处理器完成的,在Bean后处理器中判断Bean是否是Chinese实例,然后设置它的name属性。
容器中一旦注册了Bean后处理器,Bean后处理器会自动启动,在容器中每个Bean创建时自动工作,完成加入Bean后处理器需要完成的工作。
实现BeanPostProcessor接口的Bean后处理器可对Bean进行任何操作,包括完全忽略这个回调。BeanPostProcessor通常用来检查标记接口或将Bean包装成一个Proxy的事情。Spring的很多工具类,就是通过Bean后处理器完成的。
从主程序中看到,采用BeanFactory作为Spring容器时,必须手动注册BeanPost- Processor。而对于ApplicationContext,则无须手动注册。ApplicationContext可自动检测到容器中的Bean后处理器,自动注册。Bean后处理器会在Bean实例创建时,自动启动。即主程序采用如下代码,效果完全一样:
public class BeanTest
{
public static void main(String[] args)throws Exception
{
ApplicationContext ctx = new ClassPathXmlApplicationContext
("bean.xml");
Person p = (Person)factory.getBean("chinese");
System.out.println("程序中已经完成了chinese bean的实例化...");
p.useAxe();
}
}
使用ApplicationContext作为容器,无须手动注册BeanPostProcessor。因此,如果需要使用Bean后处理器,Spring容器建议使用ApplicationContext,而不是BeanFactory。
上一节介绍了一个简单的Bean后处理器,上面的Bean后处理器负责对容器中的Chinese Bean进行后处理,不管Chinese Bean如何初始化,总是将Chinese Bean的name属性设置为wawa。这种后处理看起来作用并不是特别大。
实际上,Bean后处理器完成的工作更加实际,例如生成Proxy。Spring框架本身提供了大量的Bean后处理器,这些后处理器负责对容器中的Bean进行后处理。
下面是Spring提供的两个常用的后处理器:
● BeanNameAutoProxyCreator,根据Bean实例的name属性,创建Bean实例的代理。
● DefaultAdvisorAutoProxyCreator,根据提供的Advisor,对容器中所有的Bean实例创建代理。
上面提供的两个Bean后处理器,都用于根据容器中配置的拦截器创建目标Bean代理,目标代理就在目标Bean的基础上修改得到。
注意:如果需要对容器中某一批Bean进行特定的处理,可以考虑使用Bean后处理器。
除了上面提供的Bean后处理器外,Spring还提供了一种容器后处理器。Bean后处理器负责后处理容器生成的所有Bean,而容器后处理器则负责后处理容器本身。
容器后处理器必须实现BeanFactoryPostProcessor接口。实现该接口必须实现如下一个方法:
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
实现该方法的方法体就是对Spring容器进行的处理,这种处理可以对Spring容器进行任意的扩展,当然也可以对Spring容器不进行任何处理。
类似于BeanPostProcessor,ApplicationContext可自动检测到容器中的容器后处理器,并且自动注册容器后处理器。但若使用BeanFactory作为Spring容器,则必须手动注册后处理器。
下面定义了一个容器后处理器,这个容器后处理器实现BeanFactoryPostProcessor接口,但并未对Spring容器进行任何处理,只是打印出一行简单的信息。该容器后处理器的代码如下:
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor
{
//容器后处理器对容器进行的处理在该方法中实现
public void postProcessBeanFactory(ConfigurableListableBeanFactory
beanFactory)
throws BeansException
{
System.out.println("程序对Spring所做的BeanFactory的初始化没有意
见...");
}
}
将该Bean作为普通Bean部署在容器中,然后使用ApplicationContext作为容器,容器会自动调用BeanFactoryPostProcessor处理Spring容器。程序执行效果如下:
[java] 程序对Spring所做的BeanFactory的初始化没有意见...
实现BeanFactoryPostProcessor接口的Bean后处理器不仅可对BeanFactory执行后处理,也可以对ApplicationContext容器执行后处理。容器后处理器还可用来注册额外的属性编辑器。
注意:Spring没有提供ApplicationContextPostProcessor。也就是说,对于Application- Context容器,一样使用BeanFactoryPostProcessor作为容器后处理器。
Spring已提供如下两个常用的容器后处理器,包括:
● PropertyResourceConfigurer,属性占位符配置器。
● PropertyPlaceHolderConfigurer,另一种属性占位符配置器。
下面将详细介绍这两种常用的容器后处理器。
Spring提供了PropertyPlaceholderConfigurer,它是一个容器后处理器,负责读取Java属性文件里的属性值,并将这些属性值设置到Spring容器定义中。
通过使用PropertyPlaceholderConfigurer后处理器,可以将Spring配置文件中的部分设置放在属性文件中设置。这种配置方式当然有其优势:可以将部分相似的配置(如数据库的urls、用户名和密码)放在特定的属性文件中,如果只需要修改这部分配置,则无须修改Spring配置文件,修改属性文件即可。
下面的配置文件配置了PropertyPlaceholderConfigurer后处理器,在配置数据源Bean时,使用了属性文件中的属性值。配置文件的代码如下:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.beans.factory.config.
PropertyPlaceholderConfigurer">
在上面的配置文件中,配置driverClass和jdbcUrl等信息时,并未直接设置这些属性的属性值,而是设置了${jdbc.driverClassName}和${jdbc.url}属性值。这表明Spring容器将从propertyConfigurer指定属性文件中搜索这些key对应的value,并为该Bean的属性值设置这些value值。
如前所述,ApplicationContext会自动检测部署在容器的容器后处理器,无须额外的注册,容器自动注册。因此,只需提供如下Java Properties文件:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/j2ee
jdbc.username=root
jdbc.password=32147
通过这种方法,可从主XML配置文件中分离出部分配置信息。如果仅需要修改数据库连接属性,则无须修改主XML配置文件,只需要修改属性文件即可。采用属性占位符的配置方式,可以支持使用多个属性文件。通过这种方式,可将配置文件分割成多个属性文件,从而降低修改配置的风险。
注意:对于数据库连接等信息集中的配置,可以将其配置在Java属性文件中,但不要过多地将Spring配置信息抽离到Java属性文件中,否则可能会降低Spring配置文件的可读性。
PropertyOverrideConfigurer是Spring提供的另一个容器后处理器,这个后处理器的额作用与上面介绍的容器后处理器作用大致相同。但也存在些许差别:PropertyOverride- Configurer使用的属性文件用于覆盖XML配置文件中的定义。即PropertyOverride- Configurer允许XML配置文件中有默认的配置信息。
如果PropertyOverrideConfigurer的属性文件有对应配置信息,XML文件中的配置信息被覆盖;否则,直接使用XML文件中的配置信息。使用PropertyOverrideConfigurer的属性文件,应是如下的格式:
beanName.property=value
beanName是属性占位符试图覆盖的Bean名,property是试图覆盖的属性名。看如下配置文件:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> class="org.springframework.beans.factory.config.
PropertyOverrideConfigurer">
上面的配置文件中,指定数据源Bean的各种属性值时,只是随意指定了几个属性值,很明显通过这几个属性值无法连接到数据库服务。
但因为Spring容器中部署了一个PropertyOverrideConfigurer的容器后处理器,而且Spring容器使用ApplicationContext作为容器,它会自动检测容器中的容器后处理器,无须额外的注册,容器自动注册该后处理器。
PropertyOverrideConfigurer后处理器读取dbconn.properties文件中的属性,用于覆盖目标Bean的属性。因此,如果属性文件中有dataSource Bean属性的设置,则配置文件中指定的属性值将没有任何作用。
dbconn.properties属性文件如下:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://wonder:3306/j2ee
dataSource.username=root
dataSource.password=32147
注意属性文件的格式必须是:
beanName.property=value
也就是说,dataSource必须是容器中真实存在的bean名,否则程序将出错。
注意:程序无法知道BeanFactory定义是否被覆盖。仅仅通过察看XML配置文件,无法知道配置文件的配置信息是否被覆盖。如有多个PorpertyOverrideConfigurer对同一Bean属性定义了覆盖,最后一个覆盖获胜。
posted @ 2009-07-19 10:12 jadmin 阅读(2) 评论(0) 编辑
前面介绍了Hibernate的一些相关知识点,距离Hibernate进入实际开发还有一段路要走。Hibernate作为持久层解决方案,必须与其他表现层技术组合在一起才可形成一个J2EE开发框架。经常看到网上一些朋友给出的Hibernate入门示例,居然在JSP页面中访问Hibernate Configuratioin对象。甚至看到某些所谓的精通J2EE书籍,也居然在JSP页面中访问Hibernate的Configuration对象——这种现状非常让人担忧,Hibernate并不是万金油,并不是说项目中使用Hibernate就怎么了不起了,而是通过使用Hibernate,可以让J2EE应用架构更科学,可以让开发者以更好的面向对象的方式进行项目开发。
反过来说,即使不使用Hibernate,而使用普通的JDBC持久化解决方案,也不应该在JSP(表现层)访问到JDBC API(持久层API)。下面介绍如何让Hibernate和Struts进行整合,整合Spring部分将在后面章节介绍。
工厂模式是指当应用程序中A组件需要B组件协助时,并不是直接创建B组件的实例,而是通过B组件的工厂——该工厂可以生成某一个类型组件的实例。在这种模式下,A组件无须与B组件以硬编码方式耦合在一起,而只需要与B组件的工厂耦合。
对于A组件而言,它只关心工厂生产的实例是否满足某种规范,即实现了某个接口(满足接口规范,即可供自己正常调用)。这种模式提供了对象之间清晰的角色划分,降低了程序的耦合。
接口产生的全部实例通常实现相同接口,接口里定义全部实例共同拥有的方法,这些方法在不同的实现类中实现方式不同。程序调用者无须关心方法的具体实现,从而降低了系统异构的代价。
下面是工厂模式的示例代码:
//Person接口定义
public interface Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name);
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name);
}
该接口定义Person的规范,该接口必须拥有两个方法:能打招呼、能告别。规范要求实现该接口的类必须具有这两个方法:
//American类实现Person接口
public class American implements Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name)
{
return name + ",Hello";
}
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name)
{
return name + ",Good Bye";
}
}
下面是实现Person接口的另一个实现类Chinese
public class Chinese implements Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name)
{
return name + ",您好";
}
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name)
{
return name + ",下次再见";
}
}
然后看Person工厂的代码:
public class PersonFactory
{
/**
* 获得Person实例的工厂方法
* @ param ethnic 调用该实例工厂方法传入的参数
* @ return返回Person实例
*/
public Person getPerson(String ethnic)
{
//根据参数返回Person接口的实例
if (ethnic.equalsIgnoreCase("chin"))
{
return new Chinese();
}
else
{
return new American();
}
}
}
最简单的工厂模式的框架基本如上所示。
主程序部分仅仅需要与工厂耦合,而无须与具体的实现类耦合在一起。下面是主程序部分:
public class FactroyTest
{
public static void main(String[] args)
{
//创建PersonFactory的实例,获得工厂实例
PersonFactory pf = new PersonFactory();
//定义接口Person的实例,面向接口编程
Person p = null;
//使用工厂获得Person的实例
p = pf.getPerson("chin");
//下面调用Person接口的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
//使用工厂获得Person的另一个实例
p = pf.getPerson("ame");
//再次调用Person接口的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
}
}
主程序从Person接口的具体类中解耦出来,而且程序调用者无须关心Person的实例化过程,角色划分清晰。主程序仅仅与工厂服务定位结合在一起,获得工厂的引用,程序将可获得所有工厂产生的实例。具体类的变化,重要接口不发生任何改变,调用者程序代码部分几乎无须发生任何改动。
第1章介绍了J2EE应用的架构,最上面的表现层,表现层与MVC框架的控制器交互,控制器负责调用业务逻辑组件的业务逻辑方法来处理用户请求,而业务逻辑组件则依赖于DAO组件提供的数据库原子操作,这种模式也被称为DAO模式。
由上面关于J2EE应用架构的介绍可见,控制器总是依赖于业务逻辑组件,而业务逻辑组件总是依赖于DAO组件。也就是说,控制器需要调用业务逻辑组件的方法,而业务逻辑组件需要调用DAO组件的方法。
DAO模式的分层非常清晰,持久层访问被封装在DAO层下,而决不会扩散到业务逻辑层,更不会在JSP页面(表现层)中进行持久层访问。
注意:即使在早期的Model 1(使用JSP + JavaBean创建应用的模式,没有使用MVC设计模式)模式下,持久层访问也被封装在JavaBean中完成,而不是直接在JSP页面中进行数据库访问。对于直接在JSP中访问持久层API的做法,可以说根本不了解J2EE开发。
那么控制器采用怎样的方式访问业务逻辑组件呢?应该采用工厂模式,让控制器与业务逻辑组件的实现类分离,仅与业务逻辑工厂耦合;同样,业务逻辑组件也应该采用工厂模式访问DAO模式,而不是直接与DAO实现类耦合。
后面的案例部分会介绍更实际的整合策略,此处仅仅介绍DAO模式下两个工厂模式策略。
在J2EE应用开发中,可扩展性是一个随时需要关注的问题。而DAO组件是经常需要增加的项目组件,如果每次需要增加一个DAO组件都需要修改代码是相当让人沮丧的事情。为了避免这种情况,采用XML配置文件来管理所有的DAO组件,这种DAO组件配置文件的代码如下:
查看上面的配置文件可以看出,应用中有配置了两个DAO组件,因为每个DAO组件在J2EE应用中仅需要一个实例就足够了,因此DAO工厂类提供了一个缓存池来缓存每个DAO实例,并负责在应用启动时创建所有的DAO。
下面是DAO工厂类的代码:
public class DaoFactory
{
//用于缓存DAO实例的Map对象
private Map
//将DAO工厂写成单态模式
private static DaoFactory df;
//DAO工厂的构造器
private DaoFactory()throws Exception
{
//使用SAXReader来负责解析daoContext.xml配置文档
Document doc = new SAXReader().read(new File(ConstantsUtil.realPath
+ "\\daoContext.xml"));
//获取文档的根文档
Element root = doc.getRootElement();
//获取daoContext根元素的所有子元素
List el = root.elements();
for (Iterator it = el.iterator();it.hasNext() ; )
{
//每个子元素对应一个DAO组件
Element em = (Element)it.next();
String id = em.attributeValue("id");
//获取实现类
String impl = em.attributeValue("class");
//通过反射,根据类名创建DAO组件的实例
Class implClazz = Class.forName(impl);
Dao d = (Dao)implClazz.newInstance();
//将创建的DAO组件放入缓存池中
daoMap.put(id, d);
}
}
//单态模式必须提供一个入口方法来创建DAO工厂的方法
public static DaoFactory instance()throws Exception
{
//如果DAO工厂还未创建
if (df == null)
{
df = new DaoFactory();
}
return df;
}
//下面的方法用于根据DAO组件ID获取DAO组件
public Dao getDao(String id)
{
return daoMap.get(id);
}
}
通过上面的工厂类代码可以看出,DAO工厂负责初始化所有的DAO组件。系统每增加一个DAO组件,无须再修改任何代码,仅仅需要在daoContext.xml配置文件中增加配置即可。
注意:这种整合策略非常优秀。可扩展性很好,如果应用需要增加一个DAO组件,只需要修改配置文件,并提供相应的DAO组件实现即可。而且,如果有一天需要重构DAO组件,只须提供修改过的DAO组件实现类,而业务逻辑组件无须任何改变。
业务逻辑组件代码无须与DAO实现类耦合,业务逻辑组件的代码面向DAO组件的接口编程,将业务逻辑组件和DAO组件的耦合降低到接口层次。
与此类似的是,业务逻辑组件完全可以采用这种编程模式,业务逻辑组件的配置文件代码如下:
业务逻辑组件工厂同样可根据该配置文件来初始化所有业务逻辑组件,并将业务逻辑组件放入缓存池中,让控制器与业务逻辑组件的耦合降低到接口层次。业务逻辑组件的工厂类代码如下:
public class AppFactory
{
private Map
//业务逻辑组件工厂采用单态模式
private static AppFactory df;
//业务逻辑组件工厂的私有构造器
private AppFactory()throws Exception
{
//使用SAXReader来负责解析appContext.xml配置文档
Document doc = new SAXReader().read(new File(ConstantsUtil.realPath
+ "\\appContext.xml"));
//获取文档的根文档
Element root = doc.getRootElement();
//获取appContext根元素的所有子元素
List el = root.elements();
for (Iterator it = el.iterator();it.hasNext() ; )
{
//每个app元素对应一个业务逻辑组件
Element em = (Element)it.next();
String id = em.attributeValue("id");
//根据配置文件指定的业务逻辑组件实现类来创建业务逻辑组件实例
String impl = em.attributeValue("class");
Class implClazz = Class.forName(impl);
Object d = implClazz.newInstance();
//将业务逻辑组件放入缓存池中
appMap.put(id , d);
}
}
//单态模式必须提供入口方法,用于创建业务逻辑组件工厂
public static AppFactory instance()throws Exception
{
//如果业务逻辑组件工厂为空
if (df == null)
{
df = new AppFactory();
}
return df;
}
//根据业务逻辑组件的id属性获取业务逻辑组件
public Object getApp(String id)
{
//直接从缓存池中取出业务逻辑组件,并返回
return appMap.get(id);
}
}
从某种程度上来讲,这种方式与后来Spring的控制反转(Inversion of Control,IoC)容器有异曲同工之妙,但Spring的IoC容器则提供了更多的功能。
上面的两个类中都用到了一个ConstantsUtil,它仅用于保存一个全局变量,有一个public static的realPath属性,该属性用于保存应用在服务器中的路径。
posted @ 2009-07-19 10:08 jadmin 阅读(1) 评论(0) 编辑
通常,Hibernate执行持久化过程中,应用程序无法参与其中。所有的数据持久化操作,对用户都是透明的,用户无法插入自己的动作。
通过事件框架,Hibernate允许应用程序能响应特定的内部事件,从而允许实现某些通用的功能,或对Hibernate功能进行扩展。
Hibernate的事件框架由两个部分组成:
● 拦截器机制,对于特定动作拦截,回调应用中的特定动作。
● 事件系统,重写Hibernate的事件监听器。
通过Interceptor接口,可以从Session中回调应用程序的特定方法,这种回调机制可让应用程序在持久化对象被保存、更新、删除或加载之前,检查并修改其属性。
通过Interceptor接口,可以在数据进入数据库之间,对数据进行最后的检查,如果数据不符合要求,可以修改数据,从而避免非法数据进入数据库。当然,通常无须这样做,只是在某些特殊的场合下,才考虑使用拦截器完成检查功能。
使用拦截器可按如下步骤进行:
(1)定义实现Interceptor接口的拦截器类;
(2)通过Session启用拦截器,或者通过Configuration启用全局拦截器。
下面是一个拦截器的示例代码,该拦截器没有进行任何实际的操作,仅仅打印出标志代码:
public class MyInterceptor extends EmptyInterceptor
{
//更新的次数
private int updates;
//插入的次数
private int creates;
//删除数据时,将调用onDelete方法
public void onDelete(Object entity,Serializable id,Object[]
state,String[] propertyNames, Type[] types)
{
//do nothing
}
//同步Session和数据库中的数据
public boolean onFlushDirty(Object entity, Serializable id, Object[]
currentState, Object[] previousState, String[] propertyNames, Type[]
types)
{
//每同步一次,修改的累加器加1
updates++;
for ( int i=0; i < propertyNames.length; i++ )
{
if ( "lastUpdateTimestamp".equals( propertyNames[i] ) )
{
currentState[i] = new Date();
return true;
}
}
return false;
}
//加载持久化实例时,调用该方法
public boolean onLoad(Object entity,Serializable id,Object[]
state,String[] propertyNames,Type[] types)
{
System.out.println("========================");
for ( int i=0; i < propertyNames.length; i++ )
{
if ( "name".equals( propertyNames[i] ) )
{
System.out.println(state[i]);
state[i] = "aaa";
return true;
}
}
return false;
}
//保存持久化实例时,调用该方法
public boolean onSave(Object entity,Serializable id,Object[]
state,String[] propertyNames,Type[] types)
{
creates++;
for ( int i=0; i { if ( "createTimestamp".equals( propertyNames[i] ) ) { state[i] = new Date(); return true; } } return false; } //提交刷新 public void postFlush(Iterator entities) { System.out.println("创建的次数: " + creates + ", 更新的次数: " + updates); } public void preFlush(Iterator entities) { updates=0; creates=0; } //事务提交前,触发该方法 public void beforeTransactionCompletion(Transaction tx) { System.out.println("事务即将结束"); } //事务提交后,触发该方法 public void afterTransactionCompletion(Transaction tx) { System.out.println("事务已经结束"); } } 在上面的拦截器实现类中,实现了很多方法,这些方法都是在Hibernate执行特定动作时自动调用。 完成了拦截器的定义,下面是关于拦截器的使用。拦截器的使用有两种方法: ● 通过SessionFactory的openSession(Interceptor in)方法打开一个带局部拦截器的Session。 ● 通过Configuration的setInterceptor(Interceptor in)方法设置全局拦截器。 下面是使用局部拦截器的示例代码: public class HibernateUtil { //静态类属性 SessionFactory public static final SessionFactory sessionFactory; //静态初始化块,完成静态属性的初始化 static { try { //采用默认的hibernate.cfg.xml来启动一个Configuration的实例 Configuration configuration=new Configuration().configure(); //由Configuration的实例来创建一个SessionFactory实例 sessionFactory = configuration.buildSessionFactory(); } catch (Throwable ex) { System.err.println("初始化sessionFactory失败." + ex); throw new ExceptionInInitializerError(ex); } } //ThreadLocal是隔离多个线程的数据共享,不存在多个线程之间共享资源,因此不再需要 对线程同步 public static final ThreadLocal session = new ThreadLocal(); //不加拦截器的打开Session方法 public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); //如果该线程还没有Session,则创建一个新的Session if (s == null) { s = sessionFactory.openSession(); //将获得的Session变量存储在ThreadLocal变量的Session里 session.set(s); } return s; } //加拦截器的打开Session方法 public static Session currentSession(Interceptor it) throws HibernateException { Session s = (Session) session.get(); //如果该线程还没有Session,则创建一个新的Session if (s == null) { //以拦截器创建Session对象 s = sessionFactory.openSession(it); //将获得的Session变量存储在ThreadLocal变量的Session里 session.set(s); } return s; } //关闭Session对象 public static void closeSession() throws HibernateException { Session s = (Session) session.get(); if (s != null) s.close(); session.set(null); } } 上面的Hibernate工具类提供了两个currentSession方法,分别用于不使用拦截器获取Session对象和使用拦截器获取Session对象。 下面是主程序使用拦截器的代码片段: private void testUser() { //以拦截器开始Session Session session = HibernateUtil.currentSession(new MyInterceptor()); //开始事务 Transaction tx = session.beginTransaction(); //执行下面的代码时,可以看到系统回调onSave等方法 /* User u = new User(); u.setName("Yeeku Lee"); u.setAge(28); u.setNationality("中国"); session.persist(u); u.setAge(29); u.setAge(30); session.persist(u); */ //执行下面的代码时,可以看到系统回调onLoad等方法 Object o = session.load(User.class , new Integer(1)); System.out.println(o); User u = (User)o; System.out.println(u.getName()); //提交事务时,可以看到系统回调事务相关方法 tx.commit(); HibernateUtil.closeSession(); } Hibernate 3的事件系统是功能更强大的事件框架,事件系统可以替代拦截器,也可以作为拦截器的补充来使用。 基本上,Session接口的每个方法都有对应的事件。如LoadEvent和FlushEvent等。当Session调用某个方法时,Hibernate Session会生成对应的事件,并激活对应的事件监听器。 系统默认监听器实现的处理过程,完成了所有的数据持久化操作,包括插入和修改等操作。如果用户定义了自己的监听器,则意味着用户必须完成对象的持久化操作。 例如,可以在系统中实现并注册LoadEventListener监听器,该监听器负责处理所有调用Session的load()方法的请求。 监听器是单态模式对象,即所有同类型的事件处理共享同一个监听器实例,因此监听器不应该保存任何状态,即不应该使用成员变量。 使用事件系统可按如下步骤进行: (1)实现自己的事件监听器类; (2)注册自定义事件监听器,代替系统默认的事件监听器。 实现用户的自定义监听器有如下3个方法: ● 实现对应的监听器接口,这是不可思议的,实现接口必须实现接口内的所有方法,关键是必须实现Hibernate对应的持久化操作,即数据库访问,这意味着程序员完全取代了Hibernate的底层操作。 ● 继承事件适配器,可以选择性地实现需要关注的方法,但依然试图取代Hibernate完成数据库的访问,这也不太现实。 ● 继承系统默认的事件监听器,扩展特定方法。 实际上,前两种方法很少使用。因为Hibernate的持久化操作也是通过这些监听器实现的,如果用户取代了这些监听器,则应该自己实现所有的持久化操作,这意味着用户放弃了Hibernate的持久化操作,而改为自己完成Hibernate的核心操作。 通常推荐采用第三种方法实现自己的事件监听器。Hibernate默认的事件监听器都被声明成non-final,从而方便用户继承。 下面是用户自定义监听器的示例: //自定义LoadListener,继承默认的DefaultLoadEventListener实现类 public class MyLoadListener extends DefaultLoadEventListener { //在LoadEventListener接口仅仅定义了这个方法 public Object onLoad(LoadEvent event, LoadEventListener.LoadType loadType)throws HibernateException { //先调用父类的onLoad方法,从而完成默认的持久化操作 Object o = super.onLoad(event, loadType); //加入用户的自定义处理 System.out.println("自定义的load事件"); System.out.println(event.getEntityClassName() + "==========" + event.getEntityId()); return o; } } 下面还有一个MySaveListener,用于监听SaveEvent事件: //自定义SavaListener,继承默认的DefaultSaveEventListener实现类 public class MySaveListener extends DefaultSaveEventListener { //该方法完成实际的数据插入动作 protected Serializable performSaveOrUpdate(SaveOrUpdateEvent event) { //先执行用户自定义的操作 System.out.println(event.getObject()); //调用父类的默认持久化操作 return super.performSaveOrUpdate(event); } } 注意:扩展用户自定义监听器时,别忘了在方法中调用父类的对应方法。 注册用户自定义监听器也有两种方法: ● 编程式,通过使用Configuration对象编程注册。 ● 声明式,在Hibernate的XML格式配置文件中进行声明,使用Properties格式的配置文件将无法配置自定义监听器。 下面的示例代码,通过编程方式使用自定义监听器: public class HibernateUtil2 { //静态类属性 SessionFactory public static final SessionFactory sessionFactory; //静态初始化块,完成静态属性的初始化 static { try { Configuration cfg = new Configuration(); //注册loadEventListener监听器 cfg.getSessionEventListenerConfig().setLoadEventListener ( new MyLoadListener() ); //注册saveListener监听器 cfg.getSessionEventListenerConfig().setSaveEventListener (new MySaveListener() ); //由Configuration实例来创建一个SessionFactory实例 sessionFactory = cfg.configure().buildSessionFactory(); } catch (Throwable ex) { System.err.println("初始化sessionFactory失败." + ex); throw new ExceptionInInitializerError(ex); } } //ThreadLocal是隔离多个线程的数据共享,不存在多个线程之间共享资源,因此不再需要 对线程同步 public static final ThreadLocal session = new ThreadLocal(); //不加拦截器的打开Session方法 public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); //如果该线程还没有Session,则创建一个新的Session if (s == null) { s = sessionFactory.openSession(); //将获得的Session变量存储在ThreadLocal变量的Session里 session.set(s); } return s; } //关闭Session对象 public static void closeSession() throws HibernateException { Session s = (Session) session.get(); if (s != null) s.close(); session.set(null); } } 如果不想修改代码,也可以在配置文件中使用事件监听器,注册事件监听器的Hibernate配置文件代码如下:
"-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0. dtd"> 使用配置文件注册事件监听器虽然方便,但也有不利之处,通过配置文件注册的监听器不能共享实例。如果多个 注意:虽然监听器类实现了特定监听器的接口,在注册的时候还要明确指出注册的事件。这是因为一个类可能实现多个监听器的接口,注册时明确指定要监听的事件,可以使得启用或者禁用某个事件监听的配置工作更简单。 posted @ 2009-07-19 09:42 jadmin 阅读(2) 评论(0) 编辑4.8.2 事件系统
每个业务逻辑方法都是由一系列的数据库访问完成,这一系列的数据访问可能会修改多条数据记录,这系列的修改应该是一个整体,绝不能仅修改其中的几条。也就是说,多个数据库原子访问应该绑定成一个整体——这就是事务。事务是一个最小的逻辑执行单元,整个事务不能分开执行,要么同时执行,要么同时放弃执行。
事务是一步或几步基本操作组成的逻辑执行单元,这些基本操作作为一个整体执行单元,它们要么全部执行,要么全部取消,绝不能仅仅执行部分。一般而言,每次用户请求,对应一个业务逻辑方法,一个业务逻辑方法往往具有逻辑上的原子性,应该使用事务。例如,一个转账操作,对应修改两个账户的余额,这两个账户的修改要么同时生效,要么同时取消——同时生效是转账成功,同时取消是转账失败;但不可只修改其中一个账户,那将破坏数据库的完整性。
通常来讲,事务具备如下4个特性:原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持续性(durability)。这4个特性也简称为ACID性。
● 原子性:事务是应用中最小执行单位,就如原子是自然界最小颗粒,具有不可再分的特征一样。事务是应用中不可再分的最小逻辑执行体。
● 一致性:事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库,此时,数据库就处于一种不正确的状态。比如银行在两个账户之间转账,从A账户向B账户转入1000元。系统先减少A账户的1000元,然后再为B账户增加1000元。如果全部执行成功,数据库处于一致性状态。如果仅执行完A账户金额的修改,而没有增加B账户的金额,则数据库就处于不一致性状态。因此,一致性是通过原子性来保证的。
● 隔离性:各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务,都具有隔离性。也即并发执行的事务之间不能互相影响。
● 持续性:持续性也称为持久性(persistence),指事务一旦提交,对数据所做的任何改变,都要记录到永久存储器中,通常保存进物理数据库。
Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定行为。Hibernate只添加自动版本管理,而不会锁定内存中的对象,也不会改变数据库事务的隔离级别。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)进行数据库访问。
Hibernate中SessionFactory对象的创建代价很高,它是线程安全的对象,被设计成可以为所有的应用程序线程所共享。通常,SessionFactory会在应用程序启动时创建,一旦创建了SessionFactory将不会轻易关闭,只有当应用关闭时,SessionFactory才会关闭。
而Session的对象是轻量级的,它也是线程不安全的。对于单个业务进程单个工作单元而言,Session只被使用一次。创建Session时,并不会立即打开与数据库之间的连接,Session只在需要进行数据库操作时,才会获取JDBC连接。因此,打开和关闭Session,并不会对性能造成很大的影响。甚至即使无法确定一个请求是否需要数据访问,也可以打开Session对象,因为如果不进行数据库访问,Session不会获取JDBC连接。
相反,数据库事务应该尽可能的短。从而,降低数据库锁定造成的资源争用。数据库长事务会导致应用程序无法承载高并发的负荷。
由上面的介绍可知,Hiberante的Session和事务是紧密相关的,因为事务是通过Session来打开的。那么事务的范围是多大?单个Session可以跨越多个数据库事务吗?事务和Session的对应关系又如何呢?下面将介绍Hibernate Session和事务的关系。
数据库操作必须在Hibernate的Session管理下进行,但不推荐因为一次简单的数据库原子调用,就打开和关闭一次Session,数据库事务也是如此。因为,对于一次原子操作打开的事务没有任何意义——事务应该是将多个操作步骤组合成一个逻辑整体。
事务是按顺序发送并组成一个逻辑整体的原子操作单元。
注意:也就是说单个的SQL语句发送之后,自动事务提交模式失效了。这种自动提交模式仅为SQL控制台设计,在实际项目没有太大的实用价值。Hibernate禁止事务立即自动提交模式,或者让应用服务器禁止事务自动提交。
通常,建议每个请求对应一个Session。在这种模式下,来自客户端的请求被发送到服务器端,此处可能对应一个业务逻辑方法。在这个业务逻辑方法内,一个新的Hibernate Session被打开,然后开始事务,在事务内执行这个操作单元中所有的数据库操作。一旦操作完成,需要发送给客户端的响应也准备就绪。此时,提交事务,然后关闭Session。在这种模式下,Session和用户请求是一对一的关系,这是一种理想的Session管理模式。
为了达到这种效果,推荐使用一个ThreadLocal变量,把Session绑定到处理客户端请求的线程上去。这种方式可以让运行在该线程上的所有程序代码轻松地访问Session。也可以在一个ThreadLocal变量中保持事务上下文环境,不过这依赖于所选择的数据库事务划分机制。这种实现模式被称之为ThreadLocal Session和Open Session in View。
下面是一个HibernateUtil类,该类将Hibernate Session存放在一个ThreadLocal变量中,对于同一个线程的请求,将可以轻松访问该Session。
public class HibernateUtil
{
public static final SessionFactory sessionFactory;
//静态初始化块,使用该类时使用该代码块
static
{
try
{
//采用默认的hibernate.cfg.xml来启动一个Configuration的实例
Configuration configuration=new Configuration().configure();
//由Configuration的实例来创建一个SessionFactory实例
sessionFactory = configuration.buildSessionFactory();
}
catch (Throwable ex)
{
System.err.println("初始化sessionFactory失败." + ex);
throw new ExceptionInInitializerError(ex);
}
}
//ThreadLocal是隔离多个线程的数据共享,不存在多个线程之间共享资源,因此不再需要
对线程同步
public static final ThreadLocal session = new ThreadLocal();
//该方法用于获取当前线程的Session对象
public static Session currentSession() throws HibernateException
{
Session s = (Session) session.get();
//如果该线程还没有Session,则创建一个新的Session
if (s == null)
{
s = sessionFactory.openSession();
//将获得的Session变量存储在ThreadLocal变量的Session里
session.set(s);
}
return s;
}
//该方法用于关闭当前线程里的Session
public static void closeSession() throws HibernateException
{
Session s = (Session) session.get();
if (s != null)
s.close();
session.set(null);
}
}
在上面的代码中,Hibernate Session被绑定到当前线程。当调用currentSession方法时,如果当前线程中的Session已经创建出来,那么将返回这个已经存在的Session实例。
每次请求对应一个Session的模式不仅可以用于设计操作单元,甚至很多业务处理流程都需要组合一系列的用户操作,即用户对数据库的交叉访问。
但是,对于企业应用,跨用户交互的数据库事务是无法接受的。例如,在第一个页面,用户打开对话框,打开一个特定Session装入的数据,可以随意修改对话框中的数据,修改完成后,将修改结果存入数据库。
从用户的角度来看,这个操作单元被称为应用程序长事务。在一个J2EE应用实现中,可以有很多方法来实现这种应用程序长事务。
一个比较差的做法是,当用户思考时,应用程序保持Session和数据库事务是打开的,并保持数据库锁定,以阻止并发修改,从而保证数据库事务隔离级别和原子操作。这种数据库锁定会导致应用程序无法扩展并发用户的数目。
因此,不要使用每个应用对应一次Hibernate Session的模式,也不要使用每次Http Session对应一次Hibernate Session的模式。
注意:几乎所有情况下,都不要使用每个应用对应一次Hibernate Session的模式,也不要使用每次Http Session对应一次Hibernate Session的模式。
对于这种情况,Hibernate主要有如下两种模式来解决这个问题:
● 脱管对象,如果采用每次用户请求对应一次Session的模式。那么,前面载入的实例在用户思考的过程中,始终与Session脱离,处于脱管状态。都处于与Session脱离的状态。Hibernate允许把脱管对象重新关联到Session上,并且对修改进行持久化。在这种模式下,自动版本化被用来隔离并发修改。这种模式也被称为使用脱管对象的每个请求对应一个Hibernate Session。
● 长生命周期Session,Session可以在数据库事务提交之后,断开和底层的JDBC连接。当新的客户端请求到来时,它又重新连接上底层的JDBC连接。这种模式被称为每个应用程序事务对应一个Session,因为应用程序事务相当长(跨越多个用户请求),所以也被称为每次应用事务对应一个Hibernate Session。
posted @ 2009-07-19 09:11 jadmin 阅读(5) 评论(0) 编辑
数据过滤并不是一种常规的数据查询方法,而是一种整体的筛选方法。数据过滤也可对数据进行筛选,因此,将其放在Hibernate的数据查询框架中介绍。
如果一旦启用了数据过滤器,则不管数据查询,还是数据加载,该过滤器将自动作用于所有数据,只有满足过滤条件的记录才会被选出来。
过滤器与定义在类和集合映射文件上的“where”属性非常相似。它们的区别是过滤器可以带参数,应用程序可以在运行时决定是否启用指定的过滤器,以及使用什么样的参数值。而映射文件上的“where”属性将一直生效,且无法动态传入参数。
过滤器的用法很像数据库视图,区别是视图在数据库中已经定义完成,而过滤器则还需在应用程序中确定参数值。
过滤器的使用分成三步:
(1)定义过滤器。使用filter-def元素定义过滤器;
(2)使用过滤器。使用filter元素使用过滤器;
(3)在代码中启用过滤器。
前两个步骤都是在Hibernate的映射文件中完成的,其中filter-def是hibernate-mapping元素的子元素,而filter元素是class集合元素的子元素。
filter-def元素用于定义一个过滤器,filter则将指定的过滤器应用到指定的持久化类。
一个持久化类或集合可以使用多个过滤器,而一个过滤器也可以作用于多个持久化类或集合。
看下面的映射文件示例:
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
type="java.util.Date"/> type="java.util.Date"/> table="product_category"> type="java.util.Date"/> type="java.util.Date"/> table="product_category" > class="Category" fetch="join"> condition=":asOfDate BETWEEN eff_start_date and eff_end_date"/> 在上面的配置文件中,定义了两个过滤器,过滤器的定义通过filter-def元素完成。定义过滤器时,只需要指定过滤器的名字,以及过滤器的参数即可。如Java里的一个方法声明,只有方法名和参数列表,具体的方法实现是没有的。 过滤器的过滤条件是使用过滤器时才确定的,使用过滤器通过filter元素确定,filter的condition属性用于确定过滤条件,满足该条件的记录才会被抓取到。 系统默认不启用过滤器,必须显式通过enableFilter(String filterName)才可以启用过滤器,该方法返回一个Filter实例,Filter包含setParameter方法用于为过滤器参数赋值。 一旦启用了过滤器,过滤器在整个Session内有效,所有的数据加载将自动应用该过滤条件,直到调用disableFilter方法。 看下面的使用过滤器的示例代码: private void test() throws Exception { //获取Hibernate Session对象 Session session = HibernateUtil.currentSession(); //开始事务 Transaction tx = session.beginTransaction(); //启用第一个过滤器 session.enableFilter("effectiveDate") //为过滤器设置参数 .setParameter("asOfDate", new Date()); //启动第二个过滤器 session.enableFilter("category") //为过滤器设置参数 .setParameter("catId", new Long(2)); //执行查询,该查询没有任何的查询条件 Iterator results = session.createQuery("from Product as p") .iterate(); //遍历结果集 while (results.hasNext()) { Product p = (Product)results.next(); System.out.println(p.getName()); //此处获取Product关联的种类,过滤器也将自动应用过滤 Iterator it = p.getCategories().iterator(); System.out.println(p.getCategories().size()); while (it.hasNext()) { Category c = (Category)it.next(); System.out.println(c.getName()); } } tx.commit(); HibernateUtil.closeSession(); } 通过使用过滤器定义常用的数据筛选规则,如果是临时的数据筛选,还是使用常规查询比较好。对于从前使用行列表达式视图的地方,此处可以考虑使用过滤器。 posted @ 2009-07-19 09:08 jadmin 阅读(0) 评论(0) 编辑
Hibernate还支持使用SQL查询,使用SQL查询可以利用某些数据库的特性,或者用于将原有的JDBC应用迁移到Hibernate应用上。使用命名的SQL查询还可以将SQL语句放在配置文件中配置,从而提高程序的解耦,命名SQL查询还可以用于调用存储过程。
如果是一个新的应用,通常不要使用SQL查询。
SQL查询是通过SQLQuery接口来表示的,SQLQuery接口是Query接口的子接口,因此完全可以调用Query接口的方法:
● setFirstResult(),设置返回结果集的起始点。
● setMaxResults(),设置查询获取的最大记录数。
● list(),返回查询到的结果集。
但SQLQuery比Query多了两个重载的方法:
● addEntity,将查询到的记录与特定的实体关联。
● addScalar,将查询的记录关联成标量值。
执行SQL查询的步骤如下:
(1)获取Hibernate Session对象;
(2)编写SQL语句;
(3)以SQL语句作为参数,调用Session的createSQLQuery方法创建查询对象;
(4)如果SQL语句包含参数,调用Query的setXxx方法为参数赋值;
(5)调用SQLQuery对象的addEntity或addScalar方法将选出的结果与实体或标量值关联;
(6)调用Query的list方法返回查询的结果集。
看下面的SQL查询示例:
private void test()
{
//获取Hibernate Session对象
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//编写SQL语句
String sqlString = "select {s.*} from student s where s.name like '马军'";
//以SQL语句创建SQLQuery对象
List l = session.createSQLQuery(sqlString)
//将查询到的记录与特定实体关联起来
.addEntity("s",Student.class)
//返回全部的记录集
.list();
//遍历结果集
Iterator it = l.iterator();
while (it.hasNext())
{
//因为将查询结果与Student类关联,因此返回的是Student集合
Student s = (Student)it.next();
Set enrolments = s.getEnrolments();
Iterator iter = enrolments.iterator();
while(iter.hasNext())
{
Enrolment e = (Enrolment)iter.next();
System.out.println(e.getCourse().getName());
}
}
//提交事务
tx.commit();
//关闭Session
HibernateUtil.closeSession();
}
上面的示例显示了将查询记录关联成一个实体的示例。事实上,SQL查询也支持将查询结果转换成标量值,转换成标量值可以使用addScalar方法,如:
Double max = (Double) session.createSQLQuery("select max(cat.weight) as maxWeight from cats cat")
.addScalar("maxWeight", Hibernate.DOUBLE);
.uniqueResult();
使用SQL查询,如果需要将查询到的结果转换成特定实体,就要求为选出的字段命名别名。这别名不是随意命名的,而是以“/”实例名.属性名“/”的格式命名,例如:
//依次将多个选出的字段命名别名,命名别名时都以ss作为前缀,ss是关联实体的别名
String sqlStr = "select stu.studentId as {ss.studentNumber},"
+ "stu.name as {ss.name} from "
+ "student as stu where stu.name like '杨海华'";
List l = session.createSQLQuery(sqlStr)
//将查询出的ss实例,关联到Student类
.addEntity("ss",Student.class)
.list();
在第一个示例中,以{s.*}代表该表的全部字段,且关联实例的别名也被指定为s。
注意:如果不使用{s.*}的形式,就可让实体别名和表别名互不相同。关联实体的类型时,被关联的类必须有对应的setter方法。
可以将SQL语句不放在程序中,而放在配置文件中,这种方式以松耦合的方式配置SQL语句,可以提高程序解耦。
在Hibernate的映射文件中定义查询名,然后确定查询所用的SQL语句,然后就可以直接调用该命名SQL查询。在这种情况下,不需要调用addEntity()方法,因为在配置命名SQL查询时,已经完成了查询结果与实体的关联。
下面是命名SQL查询的配置片段:
SELECT {s.*}
from student s WHERE s.name like'杨海华'
sql-query元素是hibernate-mapping元素的子元素。因此,sql-query定义的名可以直接通过Session访问,上面定义的mySqlQuery查询可以直接访问,下面是使用该命名SQL查询的示例代码:
private void testNamedSQl()
{
//获取Hibernate Session对象
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//调用命名查询,直接返回结果
List l = session.getNamedQuery("mySqlQuery")
.list();
//遍历结果集
Iterator it = l.iterator();
while (it.hasNext())
{
//在定义SQL查询时,已经将结果集与Student类关联起来
//因此,集合里的每个元素都是Student实例
Student s = (Student)it.next();
Set enrolments = s.getEnrolments();
Iterator iter = enrolments.iterator();
while(iter.hasNext())
{
Enrolment e = (Enrolment)iter.next();
System.out.println("=====================================");
System.out.println(e.getCourse().getName());
System.out.println("=====================================");
}
}
tx.commit();
HibernateUtil.closeSession();
}
Hibernate 3增加了存储过程的支持,该存储过程只能返回一个结果集。
下面是Oracle 9i的存储过程示例:
CREATE OR REPLACE FUNCTION selectAllEmployments
RETURN SYS_REFCURSOR
AS
st_cursor SYS_REFCURSOR;
BEGIN
OPEN st_cursor FOR
SELECT EMPLOYEE, EMPLOYER,
STARTDATE, ENDDATE,
REGIONCODE, EID, VALUE, CURRENCY
FROM EMPLOYMENT;
RETURN st_cursor;
END;
如果需要使用该存储过程,可以先将其定义成命名SQL查询,例如:
{ ? = call selectAllEmployments() }
调用存储过程还有如下需要注意的地方:
● 因为存储过程本身完成了查询的全部操作,所以调用存储过程进行的查询无法使用setFirstResult()/setMaxResults()进行分页。
● 存储过程只能返回一个结果集,如果存储过程返回多个结果集,Hibernate将仅处理第一个结果集,其他将被丢弃。
● 如果在存储过程里设定SET NOCOUNT ON,将有更好的性能表现。当然也可以没有该设定。
posted @ 2009-07-19 09:02 jadmin 阅读(1) 评论(0) 编辑
条件查询是更具面向对象特色的数据查询方式。条件查询可通过如下3个类完成:
● Criteria,代表一次查询。
● Criterion,代表一个查询条件。
● Restrictions,产生查询条件的工具类。
执行条件查询的步骤如下:
(1)获得Hibernate的Session对象。
(2)以Session对象创建Criteria对象。
(3)增加Criterion查询条件。
(4)执行Criteria的list等方法返回结果集。
看下面的条件查询示例:
private void test()
{
//获取Hibernate Session对象
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//创建Criteria和添加查询条件同步完成
//最后调用list方法,返回查询到的结果集
List l = session.createCriteria(Student.class)
//此处增加限制条件必须是Student已经存在的属性
.add( Restrictions.gt("studentNumber" , new Long(20050231) ) )
//如果要增加对Student的关联类的属性的限制则必须重新createCriteria()
/如果此关联属性是集合,则只要集合里任意一个对象的属性满足下面条件
.createCriteria("enrolments")即可
.add( Restrictions.gt("semester" , new Short("2") ) )
.list();
Iterator it = l.iterator();
//遍历查询到的记录
while (it.hasNext())
{
Student s = (Student)it.next();
System.out.println(s.getName());
Set enrolments = s.getEnrolments();
Iterator iter = enrolments.iterator();
while(iter.hasNext())
{
Enrolment e = (Enrolment)iter.next();
System.out.println(e.getCourse().getName());
}
}
tx.commit();
ibernateUtil.closeSession();
}
在条件查询中,Criteria接口代表一次查询,该查询本身不具备任何的数据筛选功能,Session调用createCriteria(Class clazz)方法对某个持久化类创建条件查询实例。
Criteria包含如下两个方法:
● Criteria setFirstResult(int firstResult),设置查询返回的第一行记录。
● Criteria setMaxResults(int maxResults),设置查询返回的记录数。
这两个方法与Query的这两个方法用法相似,都用于完成查询分页。
而Criteria还包含如下常用方法:
● Criteria add(Criterion criterion),增加查询条件。
● Criteria addOrder(Order order),增加排序规则。
● List list(),返回结果集。
Criterion接口代表一个查询条件,该查询条件由Restrictions负责产生,Restrictions是专门用于产生查询条件的工具类,它的方法大部分都是静态方法,常用的方法如下:
● static Criterion allEq(Map propertyNameValues),判断指定属性(由Map参数的key指定)和指定值(由Map参数的value指定)是否完全相等。
● static Criterion between(String propertyName,Object lo, Object hi),判断属性值在某个值范围之内。
● static Criterion ilike(String propertyName, Object value),判断属性值匹配某个字符串。
● static Criterion ilike(String propertyName, String value,MatchMode matchMode),判断属性值匹配某个字符串,并确定匹配模式。
● static Criterion in(String propertyName,Collection values),判断属性值在某个集合内。
● static Criterion in(String propertyName,Object[] values),判断属性值是数组元素的其中之一。
● static Criterion isEmpty(String propertyName),判断属性值是否为空。
● static Criterion isNotEmpty(String propertyName),判断属性值是否不为空。
● static Criterion isNotNull(String propertyName),判断属性值是否为空。
● static Criterion isNull(String propertyName),判断属性值是否不为空。
● static Criterion not(Criterion expression),对Criterion求否。
● static Criterion sizeEq(String propertyName, int size),判断某个属性的元素个数是否与size相等。
● static Criterion sqlRestriction(String sql),直接使用SQL语句作为筛选条件。
● static Criterion sqlRestriction(String sql, Object[] values, Type[] types),直接使用带参数占位符的SQL语句作为条件,并指定多个参数值。
● static Criterion sqlRestriction(String sql, Object value, Type type),直接使用带参数占位符的SQL语句作为条件,并指定参数值。
Order实例代表一个排序标准,Order有如下构造器:
Order(String propertyName, boolean ascending),根据propertyName排序,是否采用升序,如果后一个参数为true,采用升序排序,否则采用降序排序。
如果需要使用关联类的属性来增加查询条件,则应该对属性再次使用createCriteria方法。看如下示例:
session.createCriteria(Person.class)
.add(Restrictions.like("name" , "dd%"))
.createCriteria("addresses")
.add(Restrictions.like("addressdetail" , "上海%"))
.list();
上面的代码表示建立Person类的条件查询,第一个查询条件是直接过滤Person的属性,即选出name属性以dd开始的Person实例,第二个查询条件则过滤Person关联实例的属性,其中addresses是Person类的关联持久化类Address,而addressdetail则是Address类的属性。值得注意的是,查询并不是查询Address持久化类,而是查询Person持久化类。
注意:使用关联类的条件查询,依然是查询原有持久化类的实例,而不是查询被关联类的实例。
posted @ 2009-07-19 08:59 jadmin 阅读(5) 评论(0) 编辑
Hibernate提供了异常强大的查询体系,使用Hibernate有多种查询方式。可以选择使用Hibernate的HQL查询,或者使用条件查询,甚至可以使用原生的SQL查询语句,此外还提供了一种数据过滤功能,这些都可用于筛选目标数据。
下面分别介绍Hibernate的4种数据筛选方法:
HQL是Hibernate Query Language的缩写,HQL的语法很像SQL的语法,但HQL是一种面向对象的查询语言。因此,SQL的操作对象是数据表和列等数据对象,而HQL的操作对象是类、实例、属性等。
HQL是完全面向对象的查询语言,因此可以支持继承和多态等特征。
HQL查询依赖于Query类,每个Query实例对应一个查询对象。使用HQL查询可按如下步骤进行:
(1)获取Hibernate Session对象;
(2)编写HQL语句;
(3)以HQL语句作为参数,调用Session的createQuery方法创建查询对象;
(4)如果HQL语句包含参数,调用Query的setXxx方法为参数赋值;
(5)调用Query对象的list等方法遍历查询结果。
看下面的查询示例:
public class HqlQuery
{
public static void main(String[] args)throws Exception
{
HqlQuery mgr = new HqlQuery();
//调用查询方法
mgr.findPersons();
//调用第二个查询方法
mgr.findPersonsByHappenDate();
HibernateUtil.sessionFactory.close();
}
//第一个查询方法
private void findPersons()
{
//获得Hibernate Session
Session sess = HibernateUtil.currentSession();
//开始事务
Transaction tx = sess.beginTransaction();
//以HQL语句创建Query对象.
//执行setString方法为HQL语句的参数赋值
//Query调用list方法访问查询的全部实例
List pl = sess.createQuery("from Person p where p.myEvents.title
= :eventTitle")
.setString("eventTitle","很普通事情")
.list();
//遍历查询的全部结果
for (Iterator pit = pl.iterator() ; pit.hasNext(); )
{
Person p = ( Person )pit.next();
System.out.println(p.getName());
}
//提交事务
tx.commit();
HibernateUtil.closeSession();
}
//第二个查询方法
private void findPersonsByHappenDate()throws Exception
{
//获得Hibernate Session对象
Session sess = HibernateUtil.currentSession();
Transaction tx = sess.beginTransaction();
//解析出Date对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date start = sdf.parse("2005-01-01");
System.out.println("系统开始通过日期查找人" + start);
//通过Session的createQuery方法创建Query对象
//设置参数
//返回结果集
List pl = sess.createQuery(
"from Person p where p.myEvents.happenDate between :firstDate
and :endDate")
.setDate("firstDate",start)
.setDate("endDate",new Date())
.list();
//遍历结果集
for (Iterator pit = pl.iterator() ; pit.hasNext(); )
{
Person p = ( Person )pit.next();
System.out.println(p.getName());
}
tx.commit();
HibernateUtil.closeSession();
}
}
通过上面的示例程序,可看出查询步骤基本相似。Query对象可以连续多次设置参数,这得益于Hibernate Query的设计。
通常,setXxx方法的返回值都是void,但Hibernate Query的setXxx方法返回值是Query本身。因此,程序通过Session创建Query后,直接多次调用setXxx方法为HQL语句的参数赋值,再直接调用list方法返回查询到的全部结果即可。
Query还包含两个方法:
● setFirstResult(int firstResult),设置返回的结果集从第几条记录开始。
● setMaxResults(int maxResults),设置本次查询返回的结果数。
这两个方法用于实现Hibernate分页。
下面简单介绍HQL语句的语法。
HQL语句本身是不区分大小写的。也就是说,HQL语句的关键字和函数都是不区分大小写的。但HQL语句中所使用的包名、类名、实例名和属性名都区分大小写。
from子句是最简单的HQL语句,也是最基本的HQL语句。from关键字后紧跟持久化类的类名。例如:
from Person
表明从Person持久化类中选出全部的实例。
大部分时候,推荐为该Person的每个实例起别名。例如:
from Person as p
在上面的HQL语句中,Person持久化类中的实例的别名为p,既然 p是实例名,因此也应该遵守Java的命名规则:第一个单词的首字母小写,后面每个单词的首字母大写。
命名别名时,as关键字是可选的,但为了增加可读性,建议保留。
from后还可同时出现多个持久化类,此时将产生一个笛卡儿积或跨表的连接。
select子句用于确定选择出的属性,当然select选择的属性必须是from后持久化类包含的属性。例如:
select p.name from Person as p
select可以选择任意属性,不仅可以选择持久化类的直接属性,还可以选择组件属性包含的属性,例如:
select p.name.firstName from Person as p
select也支持将选择出的属性存入一个List对象中,例如:
select new list(p.name , p.address) from Person as p
甚至可以将选择出的属性直接封装成对象,例如:
select new ClassTest(p.name , p.address) from Person as p
前提是ClassTest支持p.name和p.address的构造器,假如p.name的数据类型是 String,p.address的数据类型是String,则ClassTest必须有如下的构造器:
ClassTest(String s1, String s2)
select还支持给选中的表达式命名别名,例如:
select p.name as personName from Person as p
这种用法与new map结合使用更普遍。如:
select new map(p.name as personName) from Person as p
在这种情形下,选择出的是Map结构,以personName为key,实际选出的值作为value。
HQL也支持在选出的属性上,使用聚集函数。HQL支持的聚集函数与SQL完全相同,有如下5个:
● avg,计算属性平均值。
● count,统计选择对象的数量。
● max,统计属性值的最大值
● min,统计属性值的最小值。
● sum,计算属性值的总和。
例如:
select count(*) from Person
select max(p.age) from Person as p
select子句还支持字符串连接符、算术运算符以及SQL函数。如:
select p.name || "" || p.address from Person as p
select子句也支持使用distinct和all关键字,此时的效果与SQL中的效果完全相同。
HQL语句被设计成能理解多态查询,from后跟的持久化类名,不仅会查询出该持久化类的全部实例,还会查询出该类的子类的全部实例。
如下面的查询语句:
from Person as p
该查询语句不仅会查询出Person的全部实例,还会查询出Person的子类,如Teacher的全部实例,前提是Person和Teacher完成了正确的继承映射。
HQL支持在from子句中指定任何Java类或接口,查询会返回继承了该类的持久化子类的实例或返回实现该接口的持久化类的实例。下面的查询语句返回所有被持久化的对象:
from java.lang.Object o
如果Named接口有多个持久化类,下面的语句将返回这些持久化类的全部实例:
from Named as n
注意:后面的两个查询将需要多个SQL SELECT语句,因此无法使用order by子句对结果集进行排序,从而,不允许对这些查询结果使用Query.scroll()方法。
where子句用于筛选选中的结果,缩小选择的范围。如果没有为持久化实例命名别名,可以直接使用属性名引用属性。
如下面的HQL语句:
from Person where name like 'tom%'
上面HQL语句与下面的语句效果相同:
from Person as p where p.name like "tom%"
在后面的HQL语句中,如果为持久化实例命名了别名,则应该使用完整的属性名。两个HQL语句都可返回name属性以tom开头的实例。
复合属性表达式加强了where子句的功能,例如如下HQL语句:
from Cat cat where cat.mate.name like "kit%"
该查询将被翻译成为一个含有内连接的SQL查询,翻译后的SQL语句如下:
select * from cat_table as table1 cat_table as table2 where table1.mate =
table2.id and table1.name like "kit%"
再看下面的HQL查询语句:
from Foo foo where foo.bar.baz.customer.address.city like"guangzhou%"
翻译成SQL查询语句,将变成一个四表连接的查询。
=运算符不仅可以被用来比较属性的值,也可以用来比较实例:
from Cat cat, Cat rival where cat.mate = rival.mate
select cat, mate
from Cat cat, Cat mate
where cat.mate = mate
特殊属性(小写)id可以用来表示一个对象的标识符。(也可以使用该对象的属性名。)
from Cat as cat where cat.id = 123
from Cat as cat where cat.mate.id = 69
第二个查询是一个内连接查询,但在HQL查询语句下,无须体会多表连接,而完全使用面向对象方式的查询。
id也可代表引用标识符。例如,Person类有一个引用标识符,它由country属性 与medicareNumber两个属性组成。
下面的HQL语句有效:
from Person as person
where person.id.country = 'AU'
and person.id.medicareNumber = 123456
from Account as account
where account.owner.id.country = 'AU'
and account.owner.id.medicareNumber = 123456
第二个查询跨越两个表Person和Account。是一个多表连接查询,但此处感受不到多表连接查询的效果。
在进行多态持久化的情况下,class关键字用来存取一个实例的鉴别值(discriminator value)。嵌入where子句中的Java类名,将被作为该类的鉴别值。例如:
from Cat cat where cat.class = DomesticCat
where子句中的属性表达式必须以基本类型或java.lang.String结尾,不要使用组件类型属性结尾,例如Account有Person属性,而Person有Name属性,Name有firstName属性。
看下面的情形:
from Account as a where a.person.name.firstName like "dd%" //正确
from Account as a where a.person.name like "dd%" //错误
HQL的功能非常丰富,where子句后支持的运算符异常丰富,不仅包括SQL的运算符,还包括EJB-QL的运算符等。
where子句中允许使用大部分SQL支持的表达式:
● 数学运算符+、–、*、/ 等。
● 二进制比较运算符=、>=、<=、<>、!=、like等。
● 逻辑运算符and、or、not等。
● in、not in、between、is null、is not null、is empty、is not empty、member of和not member of等。
● 简单的case、case ... when ... then ... else ... end和case、case when ... then ... else ... end等。
● 字符串连接符value1 || value2或使用字符串连接函数concat(value1 , value2)。
● 时间操作函数current_date()、current_time()、current_timestamp()、second()、minute()、hour()、day()、month()、year()等。
● HQL还支持EJB-QL 3.0所支持的函数或操作substring()、trim()、lower()、upper()、length()、locate()、abs()、sqrt()、bit_length()、coalesce()和nullif()等。
● 还支持数据库的类型转换函数,如cast(... as ...),第二个参数是Hibernate的类型名,或者extract(... from ...),前提是底层数据库支持ANSI cast() 和extract()。
● 如果底层数据库支持如下单行函数sign()、trunc()、rtrim()、sin()。则HQL语句也完全可以支持。
● HQL语句支持使用?作为参数占位符,这与JDBC的参数占位符一致,也可使用命名参数占位符号,方法是在参数名前加冒号 :,例如 :start_date和:x1等。
● 当然,也可在where子句中使用SQL常量,例如'foo'、69、'1970-01-01 10:00: 01.0'等。
● 还可以在HQL语句中使用Java public static final 类型的常量,例如eg.Color.TABBY。
除此之外,where子句还支持如下的特殊关键字用法。
● in与between...and可按如下方法使用:
from DomesticCat cat where cat.name between 'A' and 'B'
from DomesticCat cat where cat.name in ( 'Foo','Bar','Baz')
● 当然,也支持not in和not between...and的使用,例如:
from DomesticCat cat where cat.name not between 'A' and 'B'
from DomesticCat cat where cat.name not in ( 'Foo','Bar','Baz' )
● 子句is null与is not null可以被用来测试空值,例如:
from DomesticCat cat where cat.name is null;
from Person as p where p.address is not null;
如果在Hibernate配置文件中进行如下声明:
上面的声明表明,HQL转换SQL语句时,将使用字符1和0来取代关键字true和false。然后将可以在表达式中使用布尔表达式,例如:
from Cat cat where cat.alive = true
● size关键字用于返回一个集合的大小,例如:
from Cat cat where cat.kittens.size > 0
from Cat cat where size(cat.kittens) > 0
● 对于有序集合,还可使用minindex与maxindex函数代表最小与最大的索引序数。同理,可以使用minelement与maxelement函数代表集合中最小与最大的元素。 例如:
from Calendar cal where maxelement(cal.holidays) > current date
from Order order where maxindex(order.items) > 100
from Order order where minelement(order.items) > 10000
● 可以使用SQL函数any、some、all、exists、in操作集合里的元素,例如:
//操作集合元素
select mother from Cat as mother, Cat as kit
where kit in elements(foo.kittens)
//p的name属性等于集合中某个元素的name属性
select p from NameList list, Person p
where p.name = some elements(list.names)
//操作集合元素
from Cat cat where exists elements(cat.kittens)
from Player p where 3 > all elements(p.scores)
from Show show where 'fizard' in indices(show.acts)
注意这些结构变量size、elements、indices、minindex、maxindex、minelement、maxelement 等,只能在where子句中使用。
● where子句中,有序集合的元素(arrays, lists, maps)可以通过[ ]运算符访问。例如:
//items是有序集合属性,items[0]代表第一个元素
from Order order where order.items[0].id = 1234
//holidays是map集合属性,holidays[national day]代表其中一个元素
select person from Person person, Calendar calendar
where calendar.holidays['national day'] = person.birthDay
and person.nationality.calendar = calendar
//下面同时使用list 集合和map集合属性
select item from Item item, Order order
where order.items[ order.deliveredItemIndices[0] ] = item and order.id = 11
select item from Item item, Order order
where order.items[ maxindex(order.items) ] = item and order.id = 11
在[]中的表达式甚至可以是一个算术表达式,例如:
select item from Item item, Order order
where order.items[ size(order.items) - 1 ] = item
借助于HQL,可以大大简化选择语句的书写,提高查询语句的可读性,看下面的HQL语句:
select cust
from Product prod,
Store store
inner join store.customers cust
where prod.name = 'widget'
and store.location.name in ( 'Melbourne', 'Sydney' )
and prod = all elements(cust.currentOrder.lineItems)
如果翻译成SQL语句,将变成如下形式:
SELECT cust.name, cust.address, cust.phone, cust.id, cust.current_order
FROM customers cust,
stores store,
locations loc,
store_customers sc,
product prod
WHERE prod.name = 'widget'
AND store.loc_id = loc.id
AND loc.name IN ( 'Melbourne', 'Sydney' )
AND sc.store_id = store.id
AND sc.cust_id = cust.id
AND prod.id = ALL(
SELECT item.prod_id
FROM line_items item, orders o
WHERE item.order_id = o.id
AND cust.current_order = o.id
)
查询返回的列表(list)可以根据类或组件属性的任何属性进行排序,例如:
from Person as p
order by p.name, p.age
还可使用asc或desc关键字指定升序或降序的排序规则,例如:
from Person as p
order by p.name asc , p.age desc
如果没有指定排序规则,默认采用升序规则。即是否使用asc关键字是没有区别的,加asc是升序排序,不加asc也是升序排序。
返回聚集值的查询可以对持久化类或组件属性的属性进行分组,分组所使用的group by子句。看下面的HQL查询语句:
select cat.color, sum(cat.weight), count(cat)
from Cat cat
group by cat.color
类似于SQL的规则,出现在select后的属性,要么出现在聚集函数中,要么出现在group by的属性列表中。看下面示例:
//select后出现的id出现在group by之后,而name属性则出现在聚集函数中
select foo.id, avg(name), max(name)
from Foo foo join foo.names name
group by foo.id
having子句用于对分组进行过滤,如下:
select cat.color, sum(cat.weight), count(cat)
from Cat cat
group by cat.color
having cat.color in (eg.Color.TABBY, eg.Color.BLACK)
注意:having子句用于对分组进行过滤,因此having子句只能在有group by子句时才可以使用,没有group by子句,不能使用having子句。
Hibernate的HQL语句会直接翻译成数据库SQL语句。因此,如果底层数据库支持的having子句和group by子句中出现一般函数或聚集函数,HQL语句的having子句和order by 子句中也可以出现一般函数和聚集函数。
例如:
select cat
from Cat cat
join cat.kittens kitten
group by cat
having avg(kitten.weight) > 100
order by count(kitten) asc, sum(kitten.weight) desc
注意:group by子句与 order by子句中都不能包含算术表达式。
如果底层数据库支持子查询,则可以在HQL语句中使用子查询。与SQL中子查询相似的是,HQL中的子查询也需要使用()括起来。如:
from Cat as fatcat
where fatcat.weight > ( select avg(cat.weight) from DomesticCat cat )
如果select中包含多个属性,则应该使用元组构造符:
from Cat as cat
where not ( cat.name, cat.color ) in (
select cat.name, cat.color from DomesticCat cat
)
对于集合属性,Hibernate默认采用延迟加载策略。例如,对于持久化类Person,有集合属性scores。加载Person实例时,默认不加载scores属性。如果Session被关闭,Person实例将无法访问关联的scores属性。
为了解决该问题,可以在Hibernate映射文件中取消延迟加载或使用fetch join,例如:
from Person as p join p.scores
上面的fetch语句将会初始化person的scores集合属性。
如果使用了属性级别的延迟获取,可以使用fetch all properties来强制Hibernate立即抓取那些原本需要延迟加载的属性,例如:
from Document fetch all properties order by name
from Document doc fetch all properties where lower(doc.name) like '%cats%'
HQL查询还支持将查询所用的HQL语句放入配置文件中,而不是代码中。通过这种方式,可以大大提供程序的解耦。
使用query元素定义命名查询,下面是定义命名查询的配置文件片段:
from Person as p where p.age > ?
该命名的HQL查询可以直接通过Session访问,调用命名查询的示例代码如下:
private void findByNamedQuery()throws Exception
{
//获得Hibernate Session对象
Session sess = HibernateUtil.currentSession();
//开始事务
Transaction tx = sess.beginTransaction();
System.out.println("执行命名查询");
//调用命名查询
List pl = sess.getNamedQuery("myNamedQuery")
//为参数赋值
.setInteger(0 , 20)
//返回全部结果
.list();
//遍历结果集
for (Iterator pit = pl.iterator() ; pit.hasNext(); )
{
Person p = ( Person )pit.next();
System.out.println(p.getName());
}
//提交事务
tx.commit();
HibernateUtil.closeSession();
}
posted @ 2009-07-19 08:48 jadmin 阅读(4) 评论(0) 编辑
Hibernate完全以面向对象的方式来操作数据库,当程序里以面向对象的方式操作持久化对象时,将被自动转换为对数据库的操作。例如调用Session的delete()方法来删除持久化对象,Hibernate将负责删除对应的数据记录;当执行持久化对象的set方法时,Hibernate将自动转换为对应的update方法,修改数据库的对应记录。
问题是如果需要同时更新100 000条记录,是不是要逐一加载100 000条记录,然后依次调用set方法——这样不仅繁琐,数据访问的性能也十分糟糕。对这种批量处理的场景,Hibernate提供了批量处理的解决方案,下面分别从批量插入、批量更新和批量删除3个方面介绍如何面对这种批量处理的情形。
如果需要将100 000条记录插入数据库,通常Hibernate可能会采用如下做法:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
User u = new User (.....);
session.save(customer);
}
tx.commit();
session.close();
但随着这个程序的运行,总会在某个时候运行失败,并且抛出OutOfMemoryException(内存溢出异常)。这是因为Hibernate的Session持有一个必选的一级缓存,所有的User实例都将在Session级别的缓存区进行了缓存的缘故。
为了解决这个问题,有个非常简单的思路:定时将Session缓存的数据刷新入数据库,而不是一直在Session级别缓存。可以考虑设计一个累加器,每保存一个User实例,累加器增加1。根据累加器的值决定是否需要将Session缓存中的数据刷入数据库。
下面是增加100 000个User实例的代码片段:
private void testUser()throws Exception
{
//打开Session
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//循环100 000次,插入100 000条记录
for (int i = 0 ; i < 1000000 ; i++ )
{
//创建User实例
User u1 = new User();
u1.setName("xxxxx" + i);
u1.setAge(i);
u1.setNationality("china");
//在Session级别缓存User实例
session.save(u1);
//每当累加器是20的倍数时,将Session中的数据刷入数据库,并清空Session缓存
if (i % 20 == 0)
{
session.flush();
session.clear();
tx.commit();
tx = session.beginTransaction();
}
}
//提交事务
tx.commit();
//关闭事务
HibernateUtil.closeSession();
}
上面代码中,当i%20 == 0时,手动将Session处的缓存数据写入数据库,并手动提交事务。如果不提交事务,数据将依然缓存在事务处——未进入数据库,也将引起内存溢出的异常。
这是对Session级别缓存的处理,还应该通过如下配置来关闭SessionFactory的二级 缓存。
hibernate.cache.use_second_level_cache false
注意:除了要手动清空Session级别的缓存外,最好关闭SessionFactory级别的二级缓存。否则,即使手动清空Session级别的缓存,但因为在SessionFactory级别还有缓存,也可能引发异常。
上面介绍的方法同样适用于批量更新数据,如果需要返回多行数据,可以使用scroll()方法,从而可充分利用服务器端游标所带来的性能优势。下面是进行批量更新的代码片段:
private void testUser()throws Exception
{
//打开Session
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//查询出User表中的所有记录
ScrollableResults users = session.createQuery("from User")
.setCacheMode(CacheMode.IGNORE)
.scroll(ScrollMode.FORWARD_ONLY);
int count=0;
//遍历User表中的全部记录
while ( users.next() )
{
User u = (User) users.get(0);
u.setName("新用户名" + count);
//当count为20的倍数时,将更新的结果从Session中flush到数据库
if ( ++count % 20 == 0 )
{
session.flush();
session.clear();
}
}
tx.commit();
HibernateUtil.closeSession();
}
通过这种方式,虽然可以执行批量更新,但效果非常不好。执行效率不高,而且需要先执行数据查询,然后再执行数据更新,并且这种更新将是逐行更新,即每更新一行记录,都需要执行一条update语句,性能非常低下。
为了避免这种情况,Hibernate提供了一种类似于SQL的批量更新和批量删除的HQL语法。
Hibernate提供的HQL语句也支持批量的UPDATE和DELETE语法。
批量UPDATE和DELETE语句的语法格式如下:
UPDATE | DELETE FROM? ClassName [WHERE WHERE_CONDITIONS]
关于上面的语法格式有以下四点值得注意:
● 在FROM子句中,FROM关键字是可选的。即完全可以不写FROM关键字。
● 在FROM子句中只能有一个类名,该类名不能有别名。
● 不能在批量HQL语句中使用连接,显式的或隐式的都不行。但可以在WHERE子句中使用子查询。
● 整个WHERE子句是可选的。
假设,需要批量更改User类实例的name属性,可以采用如下代码片段完成:
private void testUser()throws Exception
{
//打开Session
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//定义批量更新的HQL语句
String hqlUpdate = "update User set name = :newName";
//执行更新
int updatedEntities = session.createQuery( hqlUpdate )
.setString( "newName", "新名字" )
.executeUpdate();
//提交事务
tx.commit();
HibernateUtil.closeSession();
}
从上面代码中可以看出,这种语法非常类似于PreparedStatement的executeUpdate语法。实际上,HQL的这种批量更新就是直接借鉴了SQL语法的UPDATE语句。
注意:使用这种批量更新语法时,通常只需要执行一次SQL的UPDATE语句,就可以完成所有满足条件记录的更新。但也可能需要执行多条UPDATE语句,这是因为有继承映射等特殊情况,例如有一个Person实例,它有Customer的子类实例。当批量更新Person实例时,也需要更新Customer实例。如果采用joined-subclass或union-subclass映射策略,Person和Customer实例保存在不同的表中,因此可能需要多条UPDATE语句。
执行一个HQL DELETE,同样使用 Query.executeUpdate() 方法,下面是一次删除上面全部记录的代码片段:
private void testUser()throws Exception
{
//打开Session实例
Session session = HibernateUtil.currentSession();
//开始事务
Transaction tx = session.beginTransaction();
//定义批量删除的HQL语句
String hqlUpdate = "delete User";
//执行批量删除
int updatedEntities = session.createQuery( hqlUpdate )
.executeUpdate();
//提交事务
tx.commit();
//关闭Session
HibernateUtil.closeSession();
}
由Query.executeUpdate()方法返回一个整型值,该值是受此操作影响的记录数量。实际上,Hibernate的底层操作是通过JDBC完成的。因此,如果有批量的UPDATE或DELETE操作被转换成多条UPDATE或DELETE语句,该方法返回的是最后一条SQL语句影响的记录行数。