如今,Hibernate正在迅速成为非常流行的(如果不是最流行的)J2EE O/R映射程序/数据集成框架。它为开发人员提供了处理企业中的关系数据库的整洁、简明且强大的工具。但如果外部需要访问这些已被包装在J2EE Web应用程序中的实体又该怎么办?是开发独立但相同的实体来访问数据,还是另外编写Web组件来管理内部访问的数据?
在某种程度上,这些问题是一定会发生的,对于我来说,当我的公司要向数据库中加载来自多个供应商的多种文件格式的记录时,就出现了这些问题。我考虑过以前常用的方法:用shell和SQL脚本(甚至存储过程)来加载数据。但由于数据模型过于复杂,我决定尽量利用现有的实体、Spring DAO以及Web应用程序之外的服务,并开发定制的J2SE命令行数据加载程序。
重要问题:我们是否应该这样做?
目前与Hibernate有关的大部分文档和例子都基于在容器中使用Hibernate。无论Hibernate是用于Web应用程序还是内部的“胖应用程序”,总是涉及到一个容器。这样做是有原因:容器支持各种特性,比如事务处理、线程和安全性。现在,要开发中型和企业应用程序,有一些工具是必需的。但当我们需要在容器外部访问实体对象时要怎么做?是使用现有的基础架构和代码呢,还是从另一种角度甚至还可能使用另一种语言去解决问题?当然,这个问题没有正确答案,在本文余下的部分中我将解释我所使用的方法,即,在Spring容器外重用现有的实体/POJO。
脚本语言(如:Perl、Python、Ruby,甚至是Tcl)乍一看都有一些优点。很多时候,脚本语言可以快速开发,并易于获得初始结果,它还可以绕过Hibernate底层的复杂性。有可能在短短数行内就连接到数据库、选择一些结果并将其打印到屏幕或某个日志文件中。但受数据模型的影响,事情可能(通常情况下都会)变得非常复杂。假设有一张person表,其中有一个到address表的外键,在插入数据时,address没有被正确插入,这会导致person也不能被插入:这是典型的事务问题。有人可能会辩解说在脚本语言中这个问题并不难解决,就像在主应用程序中所做的那样。但还是有问题存在:为什么要这样做?如果逻辑已经存在于应用程序中,为什么还要再次进行编码?而且这并不是唯一的问题,我们将需要复制工作和逻辑,还可能由此产生许多错误。
有些人可能认为这些都不是大问题,并用自认为是最合适的工具来解决这些问题。也许您已经由于编码之外的原因使用了某种独立的基础架构。也许您事先将数据上传到独立的数据库中并进行充分测试,然后再将数据迁移到生产数据库中。又或者您的数据库维护工作已经外包出去,您只需要将文件发送给合作伙伴公司,由他们来解决这些问题。最后,可能还有许多其他原因造成您并没有使用现有的Hibernate数据层——不管这些原因正确与否。但如果您可以并打算在应用程序之外使用现有的代码库,请继续往下读。我将介绍一些技巧,并解决一些令人头疼的问题。
配置
一旦决定在容器之外使用现有的Hibernate对象,那么首先就必须自己管理所有的配置。下文介绍的方法是使用一个独立的Java命令行应用程序。既然已经设置了Hibernate XML配置文件,那么您应该知道哪些参数是必需的,比如JNDI DataSource名称、实体映射文件以及用于记录SQL的各种属性。如果您决定使用命令行应用程序,那就一定要解决如何分析XML文件并把它添加到新配置中的问题。分析XML文档不是不可能的,但是这有时会带来一些其他的小任务。因此我建议使用常规的属性文件。属性文件的加载非常简单,而且从其中取值也很容易。下面的例子示范了配置Hibernate所需的最小属性集(没有任何实体映射)。
hibernate.dialect=net.sf.hibernate.dialect.PostgreSQLDialect
hibernate.connection.driver_class=org.postgresql.Driver
hibernate.connection.url=jdbc:postgresql://devserver/devdb
hibernate.connection.username=dbuser
hibernate.connection.password=dbpassword
hibernate.query.substitutions yes 'Y'
正如您所看到的,上面的属性指定了数据库的非标准语言,JDBC驱动类、数据库服务器名称、用户名、密码以及是否使用查询替换。一旦定义这些属性并保存到hibernate.properties文件(应该在类路径下)中,就很容易加载它们并传递给Hibernate Configuration对象。
Properties props = new Properties();
try {
props.load(props.getClass().getResourceAsStream("hibernate.properties"));
}catch(Exception e){
System.out.println("Error loading hibernate "+"properties.");
e.printStackTrace();
System.exit(0);
}
String driver = props.getProperty("hibernate.connection." + "driver_class");
String connUrl = props.getProperty("hibernate.connection.url");
String username = props.getProperty("hibernate.connection." + "username");
String password = props.getProperty("hibernate.connection.password");
// In my examples, I use Postgres, but Hibernate
// supports virtually every popular dbms out there.
Class.forName("org.postgresql.Driver");
Connection conn = DriverManager.getConnection(connUrl, username, password);
Configuration cfg = new Configuration();
cfg.setProperties( props );
SessionFactory sessions = cfg.buildSessionFactory();
Session session = sessions.openSession(conn);
上面这段代码提供了一个即时可用的Hibernate Session对象。但我们依然需要解决如何使用现有的实体映射的问题。《Hibernate in Action》一书的第2.3.1节说明了如何在实体XML映射文件中进行加载,如下:
Configuration cfg = new Configuration();
cfg.addResource("hello/Message.hbm.xml");
cfg.setProperties( System.getProperties() );
SessionFactory sessions = cfg.buildSessionFactory();
这段代码描述了如何从hello包加载Message的实体定义。但这种方式只适用于某些情况,对大部分实体来说这样做是乏味且容易出错的,这些代码必须人工维护,每次增加新的实体都要更新加载程序代码 。真令人厌烦!有一种更容易的发现并加载这些映射的方法,可以使这些映射与.jar一样经常保持最新。
首先,正如在web应用程序或企业应用程序中一样,映射文件必须保存在类路径中,这样Hibernate才能正常工作。这是一件好事,因为只需使用同样的.jar文件并找到这些映射文件名。如果在类路径中有多个.jar文件,则需要指定哪个文件包含映射。下面的代码是寻找映射的方法之一。
String cp = System.getProperty("java.class.path");
String jarFile = null;
List hbmList = null;
String[] cparr = cp.split("\:");
for(int j=0;j<cparr.length;j++){
// The following assumes our entities
// are wrapped up in a jar file
// called 'dbobjs.jar'
if(cparr[j].indexOf("dbobjs.jar") != -1)
jarFile=(cparr[j]);
}
if(jarFile != null){
JarFile jar = new JarFile(new File(jarFile));
Enumeration e = jar.entries();
if(e.hasMoreElements()){
hbmList = new ArrayList();
while(e.hasMoreElements()){
// Object comes back
// as JarFile
JarEntry entry = (JarEntry)e.nextElement();
if(entry.getName().indexOf(".hbm.xml") != -1){
hbmList.add(entry.getName());
}
}
}else {
System.out.println("Error: The entity "+ "jar dbobjs.jar was not found in " +"classpath: " + cp);
}
}
上面这段代码基本上是获取了初始化虚拟机的类路径系统属性,寻找包含实体映射文件的.jar文件,分析文件名,然后将文件名添到ArrayList中。当每个实体映射的全部名称都保存到ArrayList后,就可以传递给如下的Hibernate Configuration对象:
Configuration cfg = new Configuration();
Iterator iterator = hbmFileNames.iterator();
while(iterator.hasNext()){
cfg.addResource((String)iterator.next());
}
当Hibernate Session对象设置了正确的映射后,就可以进行下一步:使用实体。
使用Session
关于这方面的具体内容,因为可以找到各种与Hibernate、持久化和查询对象有关的文章、教程或者大量关于使用事务的例子,所以我不再介绍任何细节。而是考虑要用实体作什么,以及它们是如何影响Hibernate Session对象的。能否使用现有的业务对象甚至数据访问对象?在设置数据层时,我使用Spring以及它所提供的一些管理连接、事务和Session的类。这些对象都使用与Spring紧密集成的各种规则和关系定义在XML配置文件中。首先,DAO对象通过Spring的依赖注入(参见Bruce Tate的《Five Things I Love About Spring》)注入到服务中,然后配置服务以捕获特定的DAO异常(在XML配置文件中),这种异常是Spring可以正确处理的。虽然我觉得将Spring集成到数据加载应用程序中的工作量很大,我还是对DAO对象进行了细微的调整。这样,这些对象就可以在web应用程序之外使用了。
假设我在PersonDAO中有一个用来保存person对象的方法。由于容器已经设置好了Hibernate Session,我就不能在容器外重用这一DAO方法,因为它需要使用已经存在并完全配置好的Session对象。下面的代码是使用Spring容器所提供的Session支持后的典型PersonDAO:
import org.springframework.orm.hibernate.HibernateTemplate;
import test.pojos.Person;
public class PersonDAO extends HibernateTemplate {
public PersonDAO(){}
public Person save(Person aPerson) {
if(aPerson != null) super.save(person);
return person;
}
}
上面的类扩展了Spring的HibernateTemplate类,该类提供了使用Hibernate的所有基本方法。因为HibernateTemplate管理了大部分普通操作,您只需将注意力集中在特定的持续需求上。当然这里也应该有适当的异常操作,但作为示例来说上面的代码已经足够了。
现在,要给Session增加在容器外使用的支持方法,只需做少量改动:
import org.springframework.orm.hibernate.HibernateTemplate;
import net.sf.hibernate.Session;
import test.pojos.Person;
public class PersonDAO extends HibernateTemplate {
public PersonDAO(){}
public void setExternalSessionFactory(Session aSession){
setSessionFactory(session.getSessionFactory());
}
public Person save(Person aPerson) {
if(aPerson != null) super.save(person);
return person;
}
}
因为HibernateTemplate扩展了HibernateAccessor,我就可以从选中的任何Session对象设置SessionFactory。这是Spring高度灵活的设计之一,可以使代码重用变得更容易。
也许您现在没有使用Spring,那么就要采取完全不同的方法。假设您没有使用Spring奇妙的依赖注入,那么从JNDI中查找Session对象的代码如下:
import net.sf.hibernate.Session;
public class PersonDAO {
// This example assumes that there is a Hibernate
// Session object at the following JNDI location
// on a Tomcat 5.5 server:
// java:/comp/env/obj/hibernateSession
private Session session;
public PersonDAO(){
try {
Context initCtx = new InitialContext();
Context envCtx = (Context)
initCtx.lookup("java:comp/env");
session = (Session)envCtx.
lookup("obj/hibernateSession");
}catch(Exception e) {
e.printStackTrace();
}
}
public Person save(Person aPerson) {
if(aPerson != null) session.save(person);
return person;
}
}
上面的代码依靠应用容器使Hibernate Session对象可用。在容器之外使用这些对象的最简单的方法是再增加一个接收Session对象的构造函数:
import net.sf.hibernate.Session;
public class PersonDAO {
// This example assumes that there is a Hibernate
// Session object at the following JNDI location
// on a Tomcat 5.5 server: