如果关心开发人员的最新热点,那么您可能听说过 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面编程)。不过,像许多开发人员一样,您可能不清楚在自己的开发工作中如何使用这些技术。在本文中,通过具体介绍使用 Hibernate 和 Spring 在企业应用程序中构建一个事务持久层,您会认识到这些技术。
Hibernate 是 Java 平台上的一种流行的、容易使用的开放源代码对象关系(OR)映射框架。Spring 是一个 AOP 框架和 IOC 容器。这两种技术一起提供了本文中介绍的开发工作的基础。将使用 Hibernate 把一些持久性对象映射到关系数据库中,用 Spring 使 Hibernate 更容易使用并提供声明性事务支持。由于为示例类编写测试代码时使用了 DbUnit,我还附带介绍了一点 TDD (测试驱动的开发)的内容。
注意,本文假定读者熟悉 Java 平台上的企业开发,包括 JDBC、OR 映射内容、J2EE 设计模式如 DAO,以及声明性事务支持,如 Enterprise JavaBean (EJB)技术所提供的事务支持。理解这里的讨论不需要成为这些技术的专家,也不需要熟悉 AOP、IOC 或者 TDD,因为在本文中对这三者都做了介绍。
我将首先介绍两种开发技术,然后分析例子。
Hibernate 简介
Hibernate 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性/容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。
|
移植基于 Hibernate 的应用程序
如果应用程序必须在多个 RDBMS 系统上运行 ,那么基于 Hibernate 的应用程序可以毫不费力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和许多其他数据库。我最近甚至将一个应用程序从 MySQL 移植到 Hibernate 没有很好支持的 Firebird,而这种移植是很容易的。有关在 Postgres 和 MySQL 之间转换的案例分析,请参阅
参考资料
。
|
|
Hibernate 可以模拟继承(有几种方式)、关联(一对一或者一对多、containment 和 aggregation)和 composition。我将在本文中讨论每种关系类型的几个例子。
Hibernate 提供了一种称为 Hibernate Query Language (HQL) 的 查询语言,它类似于 JDO 的 JDOQL 和 EJB 的 EJB QL,尽管它更接近于前者。但是 Hibernate 没有就此止步:它还可以进行直接的 SQL 查询和/或使用 object criteria很容易地在运行时构成查询条件。在本文的例子中我将只使用 HQL。
与 EJB CMP CMR 不同,Hibernate 像 JDO 一样可以在 J2EE 容器内部或者外部工作,这可以让那些进行 TDD 和敏捷开发的人受益。
Spring 简介
AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。
像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。
简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。
|
关于 DbUnit
用新的框架开发而不进行单元测试,就像不带保护网走钢丝:当然可以这样做,但是很可能会受伤。我选择在有保护网的条件下开发,对我来说这个保护网就是 TDD。在有 DbUnit 之前,对依赖于数据库的代码进行测试是不太容易的。DbUnit 是 JUnit 的一个扩展,它提供了依赖于数据库的单元测试的框架。我用 DbUnit 编写本文中示例类的测试代码。虽然在本文中没有出现,不过在本文源代码中提供了 DbUnit 代码(请参阅
参考资料
)。有关 DbUnit 的介绍,请参阅 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允许创建一个可以构造对象的应用环境,然后向这些对象传递它们的协作对象。正如单词 倒置 所表明的,IOC 就像反过来的 JNDI。没有使用一堆抽象工厂、服务定位器、单元素(singleton)和直接构造(straight construction),每一个对象都是用其协作对象构造的。因此是由容器管理协作对象(collaborator)。
Spring 既是一个 AOP 框架、也是一个 IOC 容器。我记得 Grady Booch 说过,对象最好的地方是可以替换它们,而 Spring 最好的地方是它有助于您替换它们。有了 Spring,只要用 JavaBean 属性和配置文件加入依赖性(协作对象)。然后可以很容易地在需要时替换具有类似接口的协作对象。
Spring 为 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是将要用 AOP 为示例应用程序声明式地添加事务支持,与使用 EJB 技术时的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多内容,请参阅
参考资料
。
具体到业务
在本文的其余部分,所有的讨论都将基于一个实际的例子。起点是一个企业应用程序,要为它实现一个事务持久层。持久层是一个对象关系数据库,它包括像 User
、 User Group
、 Roles
和 ContactInfo
这些熟悉的抽象。
在深入到数据库的要素——查询和事务管理——之前,需要建立它的基础:对象关系映射。我将用 Hibernate 设置它,并只使用一点 Spring。
用 Hibernate 进行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组 XDoclet
标签支持 Hibernate 开发,这使得创建所需要的 *.hbm.xml 文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。关于 XDoclet
标签的更多内容,请参阅
参考资料
。
清单 1. 将 Java 类映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
标签将 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
将 JavaBean 属性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
标签声明 id 属性是主键,它将使用本机( generator-class="native"
)数据库机制生成键(例如,Oracle sequences 和 SQL Server Identity 键)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主键获得策略,不过我更愿意使用 native。 type 和 length属性用于从 Hibernate *.hbm.xml OR 映射文件生成表。这些 final 属性是可选的,因为使用的可能不是 green-field 数据库。在这个例子中,已经有数据库了,所以不需要额外的属性。( green-field 应用程序是一个新的应用程序, green-field 数据是新应用程序的一个新数据库。不会经常开发一个全新的应用程序,不过偶尔有一两次也不错)。
看过了表如何映射到类以及列如何映射到 JavaBean 属性,该使用 Hibernate 在 OR 数据库中设置一些关系了。
设置对象关系
在本节中,我将只触及 Hibernate 提供的设置对象间关系的选项的一小部分。首先设置像 User
、 User Group
、 Roles
和 ContactInfo
这些类之间的关系。其中一些关系如图 1 所示,这是数据库的验证对象模型。
图 1. 关系的图示
如您所见,在上述抽象中存在各种各样的关系。 User
与 ContactInfo
有一对一关系。 ContactInfo
的生命周期与 User
相同(用数据库的术语,UML 中的组成 aka 级联删除)。如果删除 User
,则相应的 ContactInfo
也会删除。在 User
s 与 Role
s 之间存在多对多关系(即与独立生命周期相关联)。在 Group
s 与 User
s 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是 composition (用数据库的说法,在 Group
s 和 Users
之间没有级联删除关系)。此外, User
和 Employee
有子类关系,就是说, Employee
的类型为 User
。表 1 显示了如何用 XDoclet
标签创建一些不同类型的对象关系。
表 1. 用 XDoclet 创建对象关系
关系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
组包含用户
一对多
Aggregation
双向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用户有联系信息
一对一
Composition 单向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用户与角色关联
多对多
Association
单向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇员是用户
Inheritance
用户
雇员
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中设置对象关系的更多内容,请参阅
参考资料
。
Hibernate 中的查询
Hibernate 有三种类型的查询:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅
参考资料
。
清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。
清单 2. 使用查询
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能会注意到第二个方法比第一个方法复杂得多,因为它强迫加载 users
集合。因为 Group->Users
之间的关系设置为 lazy initialize(即表 2 中 lazy="true"
),组对象需要一个活跃的会话以查询用户。在定义 Group
和 User
s 之间关系时设置这个属性为 lazy="false"
,则不需要第二个方法。在这种情况下,可能使用第一种方法 ( findGroupByName
) 列出组,用第二种方法( findPopulatedGroupByName
)查看组细节。
Spring IOC 和 Hibernate
使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。
从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置 dataSource
的。 dataSource
传递给 sessionFactory
, sessionFactory
传递给 UserDAO
。
清单 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
设置了 UserDAO
后,下一步就是定义并使用更多的查询以展示可以完成的操作。Hibernate 可以用预定义查询将查询存储到源代码之外,如清单 4 所示。
清单 4. 预定义查询
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代码定义了几个预定义查询。 预定义查询 是存储在 *.hbm.xml文件中的查询。在清单 5 中,可以看到如何执行预定义查询。
清单 5. 使用预定义查询
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查询进行时,可以在持久层中加上最后一层:使用 Spring 的事务管理。
用 Spring 管理事务
Spring 可以声明式地管理事务。例如, UserDAO.addUser
方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单 6 所示。
清单 6. 添加一组用户
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建议使用上述解决方案,因为每一个 User
都要在自己的事务中插入到数据库中。如果出现问题,那么只能添加部分用户。如果希望保留 ACID 属性(即保证所有都发生或者所有都不发生),可以通过程序进行事务管理,但是它很快就会变得一团糟了。相反,应使用 Spring 的 AOP 来支持声明式的事务,如清单 7 所示。
清单 7. 声明式管理事务
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在准备清单 7 的代码时,我重新改写了 UserDAO
并提取了其接口。这个接口现在是 UserDAO
,它的实现类是 UserDAOImpl
。这样清单 7 中的事务代码就使用了带有事务属性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。现在只要底层数据库支持,就可以在一个事务中添加所有用户。
结束语
在本文中,介绍了如何使用 Hibernate 和 Spring 实现一个事务持久层。Hibernate 是一种先进的 OR 映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。这两种技术的综合使用,使得开发人员可以编写媲美数据库厂商的代码,它可以在 J2EE 容器中运行,也可以单独运行。使用了 DbUnit (JUnit 的扩展)构建和测试本文中例子的所有代码,虽然这不是讨论的重点。
要了解有关 AOP、IOC 容器和测试驱动开发的更多内容,请参阅
参考资料
。
如果关心开发人员的最新热点,那么您可能听说过 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面编程)。不过,像许多开发人员一样,您可能不清楚在自己的开发工作中如何使用这些技术。在本文中,通过具体介绍使用 Hibernate 和 Spring 在企业应用程序中构建一个事务持久层,您会认识到这些技术。
Hibernate 是 Java 平台上的一种流行的、容易使用的开放源代码对象关系(OR)映射框架。Spring 是一个 AOP 框架和 IOC 容器。这两种技术一起提供了本文中介绍的开发工作的基础。将使用 Hibernate 把一些持久性对象映射到关系数据库中,用 Spring 使 Hibernate 更容易使用并提供声明性事务支持。由于为示例类编写测试代码时使用了 DbUnit,我还附带介绍了一点 TDD (测试驱动的开发)的内容。
注意,本文假定读者熟悉 Java 平台上的企业开发,包括 JDBC、OR 映射内容、J2EE 设计模式如 DAO,以及声明性事务支持,如 Enterprise JavaBean (EJB)技术所提供的事务支持。理解这里的讨论不需要成为这些技术的专家,也不需要熟悉 AOP、IOC 或者 TDD,因为在本文中对这三者都做了介绍。
我将首先介绍两种开发技术,然后分析例子。
Hibernate 简介
Hibernate 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性/容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。
|
移植基于 Hibernate 的应用程序
如果应用程序必须在多个 RDBMS 系统上运行 ,那么基于 Hibernate 的应用程序可以毫不费力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和许多其他数据库。我最近甚至将一个应用程序从 MySQL 移植到 Hibernate 没有很好支持的 Firebird,而这种移植是很容易的。有关在 Postgres 和 MySQL 之间转换的案例分析,请参阅
参考资料
。
|
|
Hibernate 可以模拟继承(有几种方式)、关联(一对一或者一对多、containment 和 aggregation)和 composition。我将在本文中讨论每种关系类型的几个例子。
Hibernate 提供了一种称为 Hibernate Query Language (HQL) 的 查询语言,它类似于 JDO 的 JDOQL 和 EJB 的 EJB QL,尽管它更接近于前者。但是 Hibernate 没有就此止步:它还可以进行直接的 SQL 查询和/或使用 object criteria很容易地在运行时构成查询条件。在本文的例子中我将只使用 HQL。
与 EJB CMP CMR 不同,Hibernate 像 JDO 一样可以在 J2EE 容器内部或者外部工作,这可以让那些进行 TDD 和敏捷开发的人受益。
Spring 简介
AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。
像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。
简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。
|
关于 DbUnit
用新的框架开发而不进行单元测试,就像不带保护网走钢丝:当然可以这样做,但是很可能会受伤。我选择在有保护网的条件下开发,对我来说这个保护网就是 TDD。在有 DbUnit 之前,对依赖于数据库的代码进行测试是不太容易的。DbUnit 是 JUnit 的一个扩展,它提供了依赖于数据库的单元测试的框架。我用 DbUnit 编写本文中示例类的测试代码。虽然在本文中没有出现,不过在本文源代码中提供了 DbUnit 代码(请参阅
参考资料
)。有关 DbUnit 的介绍,请参阅 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允许创建一个可以构造对象的应用环境,然后向这些对象传递它们的协作对象。正如单词 倒置 所表明的,IOC 就像反过来的 JNDI。没有使用一堆抽象工厂、服务定位器、单元素(singleton)和直接构造(straight construction),每一个对象都是用其协作对象构造的。因此是由容器管理协作对象(collaborator)。
Spring 既是一个 AOP 框架、也是一个 IOC 容器。我记得 Grady Booch 说过,对象最好的地方是可以替换它们,而 Spring 最好的地方是它有助于您替换它们。有了 Spring,只要用 JavaBean 属性和配置文件加入依赖性(协作对象)。然后可以很容易地在需要时替换具有类似接口的协作对象。
Spring 为 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是将要用 AOP 为示例应用程序声明式地添加事务支持,与使用 EJB 技术时的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多内容,请参阅
参考资料
。
具体到业务
在本文的其余部分,所有的讨论都将基于一个实际的例子。起点是一个企业应用程序,要为它实现一个事务持久层。持久层是一个对象关系数据库,它包括像 User
、 User Group
、 Roles
和 ContactInfo
这些熟悉的抽象。
在深入到数据库的要素——查询和事务管理——之前,需要建立它的基础:对象关系映射。我将用 Hibernate 设置它,并只使用一点 Spring。
用 Hibernate 进行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组 XDoclet
标签支持 Hibernate 开发,这使得创建所需要的 *.hbm.xml 文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。关于 XDoclet
标签的更多内容,请参阅
参考资料
。
清单 1. 将 Java 类映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
标签将 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
将 JavaBean 属性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
标签声明 id 属性是主键,它将使用本机( generator-class="native"
)数据库机制生成键(例如,Oracle sequences 和 SQL Server Identity 键)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主键获得策略,不过我更愿意使用 native。 type 和 length属性用于从 Hibernate *.hbm.xml OR 映射文件生成表。这些 final 属性是可选的,因为使用的可能不是 green-field 数据库。在这个例子中,已经有数据库了,所以不需要额外的属性。( green-field 应用程序是一个新的应用程序, green-field 数据是新应用程序的一个新数据库。不会经常开发一个全新的应用程序,不过偶尔有一两次也不错)。
看过了表如何映射到类以及列如何映射到 JavaBean 属性,该使用 Hibernate 在 OR 数据库中设置一些关系了。
设置对象关系
在本节中,我将只触及 Hibernate 提供的设置对象间关系的选项的一小部分。首先设置像 User
、 User Group
、 Roles
和 ContactInfo
这些类之间的关系。其中一些关系如图 1 所示,这是数据库的验证对象模型。
图 1. 关系的图示
如您所见,在上述抽象中存在各种各样的关系。 User
与 ContactInfo
有一对一关系。 ContactInfo
的生命周期与 User
相同(用数据库的术语,UML 中的组成 aka 级联删除)。如果删除 User
,则相应的 ContactInfo
也会删除。在 User
s 与 Role
s 之间存在多对多关系(即与独立生命周期相关联)。在 Group
s 与 User
s 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是 composition (用数据库的说法,在 Group
s 和 Users
之间没有级联删除关系)。此外, User
和 Employee
有子类关系,就是说, Employee
的类型为 User
。表 1 显示了如何用 XDoclet
标签创建一些不同类型的对象关系。
表 1. 用 XDoclet 创建对象关系
关系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
组包含用户
一对多
Aggregation
双向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用户有联系信息
一对一
Composition 单向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用户与角色关联
多对多
Association
单向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇员是用户
Inheritance
用户
雇员
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中设置对象关系的更多内容,请参阅
参考资料
。
Hibernate 中的查询
Hibernate 有三种类型的查询:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅
参考资料
。
清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。
清单 2. 使用查询
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能会注意到第二个方法比第一个方法复杂得多,因为它强迫加载 users
集合。因为 Group->Users
之间的关系设置为 lazy initialize(即表 2 中 lazy="true"
),组对象需要一个活跃的会话以查询用户。在定义 Group
和 User
s 之间关系时设置这个属性为 lazy="false"
,则不需要第二个方法。在这种情况下,可能使用第一种方法 ( findGroupByName
) 列出组,用第二种方法( findPopulatedGroupByName
)查看组细节。
Spring IOC 和 Hibernate
使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。
从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置 dataSource
的。 dataSource
传递给 sessionFactory
, sessionFactory
传递给 UserDAO
。
清单 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
设置了 UserDAO
后,下一步就是定义并使用更多的查询以展示可以完成的操作。Hibernate 可以用预定义查询将查询存储到源代码之外,如清单 4 所示。
清单 4. 预定义查询
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代码定义了几个预定义查询。 预定义查询 是存储在 *.hbm.xml文件中的查询。在清单 5 中,可以看到如何执行预定义查询。
清单 5. 使用预定义查询
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查询进行时,可以在持久层中加上最后一层:使用 Spring 的事务管理。
用 Spring 管理事务
Spring 可以声明式地管理事务。例如, UserDAO.addUser
方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单 6 所示。
清单 6. 添加一组用户
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建议使用上述解决方案,因为每一个 User
都要在自己的事务中插入到数据库中。如果出现问题,那么只能添加部分用户。如果希望保留 ACID 属性(即保证所有都发生或者所有都不发生),可以通过程序进行事务管理,但是它很快就会变得一团糟了。相反,应使用 Spring 的 AOP 来支持声明式的事务,如清单 7 所示。
清单 7. 声明式管理事务
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在准备清单 7 的代码时,我重新改写了 UserDAO
并提取了其接口。这个接口现在是 UserDAO
,它的实现类是 UserDAOImpl
。这样清单 7 中的事务代码就使用了带有事务属性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。现在只要底层数据库支持,就可以在一个事务中添加所有用户。
结束语
在本文中,介绍了如何使用 Hibernate 和 Spring 实现一个事务持久层。Hibernate 是一种先进的 OR 映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。这两种技术的综合使用,使得开发人员可以编写媲美数据库厂商的代码,它可以在 J2EE 容器中运行,也可以单独运行。使用了 DbUnit (JUnit 的扩展)构建和测试本文中例子的所有代码,虽然这不是讨论的重点。
要了解有关 AOP、IOC 容器和测试驱动开发的更多内容,请参阅
参考资料
。
如果关心开发人员的最新热点,那么您可能听说过 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面编程)。不过,像许多开发人员一样,您可能不清楚在自己的开发工作中如何使用这些技术。在本文中,通过具体介绍使用 Hibernate 和 Spring 在企业应用程序中构建一个事务持久层,您会认识到这些技术。
Hibernate 是 Java 平台上的一种流行的、容易使用的开放源代码对象关系(OR)映射框架。Spring 是一个 AOP 框架和 IOC 容器。这两种技术一起提供了本文中介绍的开发工作的基础。将使用 Hibernate 把一些持久性对象映射到关系数据库中,用 Spring 使 Hibernate 更容易使用并提供声明性事务支持。由于为示例类编写测试代码时使用了 DbUnit,我还附带介绍了一点 TDD (测试驱动的开发)的内容。
注意,本文假定读者熟悉 Java 平台上的企业开发,包括 JDBC、OR 映射内容、J2EE 设计模式如 DAO,以及声明性事务支持,如 Enterprise JavaBean (EJB)技术所提供的事务支持。理解这里的讨论不需要成为这些技术的专家,也不需要熟悉 AOP、IOC 或者 TDD,因为在本文中对这三者都做了介绍。
我将首先介绍两种开发技术,然后分析例子。
Hibernate 简介
Hibernate 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性/容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。
|
移植基于 Hibernate 的应用程序
如果应用程序必须在多个 RDBMS 系统上运行 ,那么基于 Hibernate 的应用程序可以毫不费力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和许多其他数据库。我最近甚至将一个应用程序从 MySQL 移植到 Hibernate 没有很好支持的 Firebird,而这种移植是很容易的。有关在 Postgres 和 MySQL 之间转换的案例分析,请参阅
参考资料
。
|
|
Hibernate 可以模拟继承(有几种方式)、关联(一对一或者一对多、containment 和 aggregation)和 composition。我将在本文中讨论每种关系类型的几个例子。
Hibernate 提供了一种称为 Hibernate Query Language (HQL) 的 查询语言,它类似于 JDO 的 JDOQL 和 EJB 的 EJB QL,尽管它更接近于前者。但是 Hibernate 没有就此止步:它还可以进行直接的 SQL 查询和/或使用 object criteria很容易地在运行时构成查询条件。在本文的例子中我将只使用 HQL。
与 EJB CMP CMR 不同,Hibernate 像 JDO 一样可以在 J2EE 容器内部或者外部工作,这可以让那些进行 TDD 和敏捷开发的人受益。
Spring 简介
AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。
像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。
简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。
|
关于 DbUnit
用新的框架开发而不进行单元测试,就像不带保护网走钢丝:当然可以这样做,但是很可能会受伤。我选择在有保护网的条件下开发,对我来说这个保护网就是 TDD。在有 DbUnit 之前,对依赖于数据库的代码进行测试是不太容易的。DbUnit 是 JUnit 的一个扩展,它提供了依赖于数据库的单元测试的框架。我用 DbUnit 编写本文中示例类的测试代码。虽然在本文中没有出现,不过在本文源代码中提供了 DbUnit 代码(请参阅
参考资料
)。有关 DbUnit 的介绍,请参阅 Philippe Girolami 的“
Control your test-environment with DbUnit and Anthill
” ( developerWorks,2004 年 4 月)。
|
|
IOC 允许创建一个可以构造对象的应用环境,然后向这些对象传递它们的协作对象。正如单词 倒置 所表明的,IOC 就像反过来的 JNDI。没有使用一堆抽象工厂、服务定位器、单元素(singleton)和直接构造(straight construction),每一个对象都是用其协作对象构造的。因此是由容器管理协作对象(collaborator)。
Spring 既是一个 AOP 框架、也是一个 IOC 容器。我记得 Grady Booch 说过,对象最好的地方是可以替换它们,而 Spring 最好的地方是它有助于您替换它们。有了 Spring,只要用 JavaBean 属性和配置文件加入依赖性(协作对象)。然后可以很容易地在需要时替换具有类似接口的协作对象。
Spring 为 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是将要用 AOP 为示例应用程序声明式地添加事务支持,与使用 EJB 技术时的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多内容,请参阅
参考资料
。
具体到业务
在本文的其余部分,所有的讨论都将基于一个实际的例子。起点是一个企业应用程序,要为它实现一个事务持久层。持久层是一个对象关系数据库,它包括像 User
、 User Group
、 Roles
和 ContactInfo
这些熟悉的抽象。
在深入到数据库的要素——查询和事务管理——之前,需要建立它的基础:对象关系映射。我将用 Hibernate 设置它,并只使用一点 Spring。
用 Hibernate 进行 OR 映射
Hibernate 使用 XML ( *.hbm.xml) 文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组 XDoclet
标签支持 Hibernate 开发,这使得创建所需要的 *.hbm.xml 文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。关于 XDoclet
标签的更多内容,请参阅
参考资料
。
清单 1. 将 Java 类映射到 DB 表
[User.java]
/**
*
@hibernate.class table="TBL_USER"
* ..
* ..
* ...
*/
public class User {
private Long id = new Long(-1);
private String email;
private String password;
.
.
.
/**
* @return
*
@hibernate.id column="PK_USER_ID"
* unsaved-value="-1"
* generator-class="native"
*/
public Long getId() {
return id;
}
...
/**
*
@hibernate.property column="VC_EMAIL"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="82"
* @return
*/
public String getEmail() {
return email;
}
/**
*
@hibernate.property column="VC_PASSWORD"
* type="string"
* update="false"
* insert="true"
* unique="true"
* not-null="true"
* length="20"
* @return
*/
public String getPassword() {
return password;
}
...
...
...
}
|
可以看到, @hibernate.class table="TBL_USER"
标签将 User
映射到 TBL_USER
表。 @hibernate.property column="VC_PASSWORD"
将 JavaBean 属性 password 映射到 VC_PASSWORD
列。 @hibernate.id column="PK_USER_ID"
标签声明 id 属性是主键,它将使用本机( generator-class="native"
)数据库机制生成键(例如,Oracle sequences 和 SQL Server Identity 键)。Hibernate 可以指定 generator-class="native"
以外的、其他可以想象的得到主键获得策略,不过我更愿意使用 native。 type 和 length属性用于从 Hibernate *.hbm.xml OR 映射文件生成表。这些 final 属性是可选的,因为使用的可能不是 green-field 数据库。在这个例子中,已经有数据库了,所以不需要额外的属性。( green-field 应用程序是一个新的应用程序, green-field 数据是新应用程序的一个新数据库。不会经常开发一个全新的应用程序,不过偶尔有一两次也不错)。
看过了表如何映射到类以及列如何映射到 JavaBean 属性,该使用 Hibernate 在 OR 数据库中设置一些关系了。
设置对象关系
在本节中,我将只触及 Hibernate 提供的设置对象间关系的选项的一小部分。首先设置像 User
、 User Group
、 Roles
和 ContactInfo
这些类之间的关系。其中一些关系如图 1 所示,这是数据库的验证对象模型。
图 1. 关系的图示
如您所见,在上述抽象中存在各种各样的关系。 User
与 ContactInfo
有一对一关系。 ContactInfo
的生命周期与 User
相同(用数据库的术语,UML 中的组成 aka 级联删除)。如果删除 User
,则相应的 ContactInfo
也会删除。在 User
s 与 Role
s 之间存在多对多关系(即与独立生命周期相关联)。在 Group
s 与 User
s 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是 composition (用数据库的说法,在 Group
s 和 Users
之间没有级联删除关系)。此外, User
和 Employee
有子类关系,就是说, Employee
的类型为 User
。表 1 显示了如何用 XDoclet
标签创建一些不同类型的对象关系。
表 1. 用 XDoclet 创建对象关系
关系
|
Java/XDoclet
|
SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
|
组包含用户
一对多
Aggregation
双向 (Group<-->Users)
|
[Group.java]
/** * * @return * * @hibernate.bag name="users" * cascade="save-update" * lazy="true" * inverse="true" * * @hibernate.collection-key * column="FK_GROUP_ID" * * @hibernate.collection-one-to-many * class="net.sf.hibernateExamples.User" */ public List getUsers() { return users; }
[User.java] /** * @hibernate.many-to-one * column="FK_GROUP_ID" * class="net.sf.hibernateExamples.Group" */ public Group getGroup() { return group; }
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_GROUP ( PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(255), VC_NAME VARCHAR(40) unique, primary key (PK_GROUP_ID) )
alter table TBL_USER add index (FK_GROUP_ID), add constraint FK_111 foreign key (FK_GROUP_ID) references TBL_GROUP (PK_GROUP_ID)
|
用户有联系信息
一对一
Composition 单向 (User-->ContactInfo)
|
[User.java]
/** * @return * * @hibernate.one-to-one cascade="all" * */ public ContactInfo getContactInfo() { return contactInfo; }
[ContactInfo.java] (Nothing to see here. Unidirectional!)
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_CONTACT_INFO ( PK_CONTACT_INFO_ID BIGINT not null, ... ... ... primary key (PK_CONTACT_INFO_ID) )
|
用户与角色关联
多对多
Association
单向 (Users-->Roles)
|
[User.java]
/** * @return * @hibernate.bag * table="TBL_JOIN_USER_ROLE" * cascade="all" * inverse="true" * * @hibernate.collection-key * column="FK_USER_ID" * * @hibernate.collection-many-to-many * class="net.sf.hibernateExamples.Role" * column="FK_ROLE_ID" * */ public List getRoles() { return roles; }
[Role.java] Nothing to see here. Unidirectional!
|
create table TBL_ROLE ( PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT, VC_DESCRIPTION VARCHAR(200), VC_NAME VARCHAR(20), primary key (PK_ROLE_ID) )
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
create table TBL_JOIN_USER_ROLE ( FK_USER_ID BIGINT not null, FK_ROLE_ID BIGINT not null )
|
雇员是用户
Inheritance
用户
雇员
|
[User.java]
/** * @hibernate.class table="TBL_USER" * discriminator-value="2" * @hibernate.discriminator column="USER_TYPE" * ... ... ... */ public class User {
[Employee.java] /** * @hibernate.subclass discriminator-value = "1" */ public class Employee extends User{
|
create table TBL_USER ( PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT, USER_TYPE VARCHAR(255) not null, FK_GROUP_ID BIGINT, VC_EMAIL VARCHAR(82) not null unique, primary key (PK_USER_ID) )
|
要了解在 Hibernate 中设置对象关系的更多内容,请参阅
参考资料
。
Hibernate 中的查询
Hibernate 有三种类型的查询:
-
Criteria, object composition
-
SQL
-
HQL
在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅
参考资料
。
清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。
清单 2. 使用查询
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
*/
public class UserDAO extends HibernateDaoSupport{
.
.
.
/**
* Demonstrates looking up a group with a HQL query
* @param email
* @return
*/
public Group findGroupByName(String name) {
return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
}
/**
* Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
* @param email
* @return
*/
public Group findPopulatedGroupByName(final String name) {
HibernateCallback callback = new HibernateCallback(){
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Group group =null;
String query = "from Group g where g.name=?";
Query queryObject = getHibernateTemplate().createQuery(session, query);
queryObject.setParameter(0, name);
group = (Group) queryObject.list().get(0);
group.getUsers().size();//force load
return group;
}
};
return (Group) getHibernateTemplate().execute(callback);
}
.
.
.
}
|
您可能会注意到第二个方法比第一个方法复杂得多,因为它强迫加载 users
集合。因为 Group->Users
之间的关系设置为 lazy initialize(即表 2 中 lazy="true"
),组对象需要一个活跃的会话以查询用户。在定义 Group
和 User
s 之间关系时设置这个属性为 lazy="false"
,则不需要第二个方法。在这种情况下,可能使用第一种方法 ( findGroupByName
) 列出组,用第二种方法( findPopulatedGroupByName
)查看组细节。
Spring IOC 和 Hibernate
使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。
从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置 dataSource
的。 dataSource
传递给 sessionFactory
, sessionFactory
传递给 UserDAO
。
清单 3. Spring IOC 和 Hibernate
<beans>
<!-- Datasource that works in any application server
You could easily use J2EE data source instead if this were
running inside of a J2EE container.
-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
<property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
<property name="username"><value>root</value></property>
<property name="password"><value></value></property>
</bean>
<!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref local="dataSource"/></property>
<!-- Must references all OR mapping files. -->
<property name="mappingResources">
<list>
<value>net/sf/hibernateExamples/User.hbm.xml</value>
<value>net/sf/hibernateExamples/Group.hbm.xml</value>
<value>net/sf/hibernateExamples/Role.hbm.xml</value>
<value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
</list>
</property>
<!-- Set the type of database; changing this one property will port this to Oracle,
MS SQL etc. -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
</props>
</property>
</bean>
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
</beans>
|
设置了 UserDAO
后,下一步就是定义并使用更多的查询以展示可以完成的操作。Hibernate 可以用预定义查询将查询存储到源代码之外,如清单 4 所示。
清单 4. 预定义查询
[User.java]
/**
* @author Richard Hightower
* ArcMind Inc. http://www.arc-mind.com
* @hibernate.class table="TBL_USER" discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
*
* @hibernate.query name="OverheadStaff"
* query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
*
* @hibernate.query name="CriticalStaff"
* query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
*
* @hibernate.query name="GetUsersInAGroup"
* query="select user from Group g join g.users user"
*
* @hibernate.query name="GetUsersNotInAGroup"
* query="select user from User user where user.group is null"
*
* @hibernate.query name="UsersBySalaryGreaterThan"
* query="from User user inner join user.contactInfo info where info.salary > ?1"
*
* @hibernate.query name="UsersBySalaryBetween"
* query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
*
* @hibernate.query name="UsersByLastNameLike"
* query="from User user join user.contactInfo info where info.lastName like ?1"
*
* @hibernate.query name="GetEmailsOfUsers"
* query="select user.email from Group g join g.users as user where g.name = ?1"
*
*/
public class User {
.
.
.
|
上述代码定义了几个预定义查询。 预定义查询 是存储在 *.hbm.xml文件中的查询。在清单 5 中,可以看到如何执行预定义查询。
清单 5. 使用预定义查询
[UserDAO.java]
/**
* Demonstrates a query that returns a String.
*/
public String[] getUserEmailsInGroup(String groupName){
List emailList =
getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
return (String [])
emailList.toArray(new String[emailList.size()]);
}
/**
* Demonstrates a query that returns a list of Users
*
* @return A list of emails of all of the users in the authentication system.
*
*/
public List getUsers(){
return getHibernateTemplate().findByNamedQuery("AllUsers");
}
/**
* Demonstrates passing a single argument to a query.
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalary(float salary){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryGreaterThan",
new Float(salary));
}
/**
* Demonstrates passing multiple arguments to a query
*
* @return A list of UserValue objects.
*
*/
public List getUsersBySalaryRange(float start, float stop){
return getHibernateTemplate()
.findByNamedQuery("UsersBySalaryBetween",
new Object[] {new Float(start), new Float(stop)});
}
|
查询进行时,可以在持久层中加上最后一层:使用 Spring 的事务管理。
用 Spring 管理事务
Spring 可以声明式地管理事务。例如, UserDAO.addUser
方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单 6 所示。
清单 6. 添加一组用户
[UserDAO.java]
/**
* @param group
*/
public void addGroup(Group group) {
getHibernateTemplate().save(group);
}
[UserDAOTest.java]
public void testAddGroupOfUsers(){
Group group = new Group();
for (int index=0; index < 10; index++){
User user = new User();
user.setEmail("rick"+index+"@foobar.com" );
user.setPassword("foobar");
group.addUser(user);
}
group.setName("testGroup");
userDAO.addGroup(group);
assertNotNull(group.getId());
Group group2 = userDAO.findPopulatedGroupByName("testGroup");
assertEquals("testGroup",group2.getName());
assertEquals(10, group2.getUsers().size());
String email = ((User)group2.getUsers().get(0)).getEmail();
assertEquals("rick0@foobar.com", email);
}
|
不建议使用上述解决方案,因为每一个 User
都要在自己的事务中插入到数据库中。如果出现问题,那么只能添加部分用户。如果希望保留 ACID 属性(即保证所有都发生或者所有都不发生),可以通过程序进行事务管理,但是它很快就会变得一团糟了。相反,应使用 Spring 的 AOP 来支持声明式的事务,如清单 7 所示。
清单 7. 声明式管理事务
[applicationContext.xml]
<!-- Pass the session factory to our UserDAO -->
<bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate.HibernateTransactionManager">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
</bean>
<bean id="userDAO"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref local="transactionManager"/></property>
<property name="target"><ref local="userDAOTarget"/></property>
<property name="transactionAttributes">
<props>
<prop key="addGroup">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
注意在准备清单 7 的代码时,我重新改写了 UserDAO
并提取了其接口。这个接口现在是 UserDAO
,它的实现类是 UserDAOImpl
。这样清单 7 中的事务代码就使用了带有事务属性 (PROPAGATION_REQUIRED)
的 UserDAO.addGroup()
方法。现在只要底层数据库支持,就可以在一个事务中添加所有用户。
结束语
在本文中,介绍了如何使用 Hibernate 和 Spring 实现一个事务持久层。Hibernate 是一种先进的 OR 映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。这两种技术的综合使用,使得开发人员可以编写媲美数据库厂商的代码,它可以在 J2EE 容器中运行,也可以单独运行。使用了 DbUnit (JUnit 的扩展)构建和测试本文中例子的所有代码,虽然这不是讨论的重点。
要了解有关 AOP、IOC 容器和测试驱动开发的更多内容,请参阅
参考资料
。
posted on 2006-05-18 20:03
崛起的程序员 阅读(600)
评论(0) 编辑 收藏 所属分类:
java