如果关心开发人员的最新热点,那么您可能听说过 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 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性 / 容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。
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 和敏捷开发的人受益。
AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。
像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。
简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。
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 使用 XML (
*.hbm.xml)
文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组
XDoclet标签支持
Hibernate 开发,这使得创建所需要的
*.hbm.xml文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。关于
XDoclet
标签的更多内容,请参阅
参考资料。
[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也会删除。在
Users 与
Roles 之间存在多对多关系(即与独立生命周期相关联)。在
Groups
与
Users 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是
composition (用数据库的说法,在
Groups 和
Users之间没有级联删除关系)。此外,
User
和
Employee有子类关系,就是说,
Employee的类型为
User。表
1 显示了如何用
XDoclet标签创建一些不同类型的对象关系。
| 关系 | Java/XDoclet | SQL DDL(由 Hibernate Schema Export 生成的 MySQL) |
|
组包含用户
一对多 Aggregation 双向 (Group<-->Users) |
[Group.java]
|
|
|
用户有联系信息
一对一 Composition 单向 (User-->ContactInfo) |
[User.java]
|
create table TBL_USER (
|
|
用户与角色关联
多对多 Association 单向 (Users-->Roles) |
[User.java]
|
create table TBL_ROLE (
|
|
雇员是用户
Inheritance 用户 雇员 |
|
|
要了解在 Hibernate 中设置对象关系的更多内容,请参阅 参考资料。
Hibernate 有三种类型的查询:
- Criteria, object composition
- SQL
- HQL
在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅 参考资料。
清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。
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和
Users 之间关系时设置这个属性为
lazy="false",则不需要第二个方法。在这种情况下,可能使用第一种方法 (
findGroupByName) 列出组,用第二种方法(
findPopulatedGroupByName)查看组细节。
使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。
从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置
dataSource的。
dataSource传递给
sessionFactory,
sessionFactory
传递给
UserDAO。
<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 所示。
[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 中,可以看到如何执行预定义查询。
[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 可以声明式地管理事务。例如,
UserDAO.addUser方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单
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 所示。
[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 容器和测试驱动开发的更多内容,请参阅 参考资料。
- 您可以参阅本文在 developerWorks 全球站点上的
英文原文.
- Hibernate 的创始人 Gavin King 为编写 Hibernate 的文档(以及框架!)设置了很高的标准。要了解 Java
平台上这种让人激动的 OR 映射工具的更多内容,请参阅 Hibernate 主页上的
Hibernate
User Guide。
- 可以通过 Rod Johnson 的 “
Introducing
the Spring framework”(ServerSide.com,2003 年 10 月),直接跟 Spring 的发明者学习有关它的更多内容。
- Matt Raible 的
AppFuse
很实用地介绍了对几个 J2EE 应用服务器上的数据库使用 Hibernate 和 Spring 。
- Juergen Hoeller 通过其教程“
Hibernate
-- Data Access with a Spring Framework”(2003 年 7 月)制定了关于 Hibernate、AOP、Spring
和事务管理的规则。
- 还想了解 Spring 的更多内容?请访问
Spring
主页。
-
DbUnit
主页是学习 Junit 这个方便扩展的第一资源。
- 有关 DbUnit 的介绍,请参阅 Philippe Girolami 的“
Control
your test-environment with DbUnit and Anthill”(
developerWorks,2004
年 4 月)。
- 当然,Andrew Glover 的“
Effective unit testing with DbUnit”(OnJava.com,2004 年 1 月)是另一个很好的资源。
- 通过 Malcolm Davis 的“
利用 Ant 和 JUnit 进行增量开发”(
developerWorks,2000
年 11 月)学习更多有关 DbUnit 及其前身 JUnit 的内容。
- 至于 AOP,它是由 Nicholas Lesiecki 一手开创的(至少我认为是这样)。阅读他的“
使用面向 Aspect 的编程改进模块性”(
developerWorks,2002
年 1 月),了解这种强大的面向对象编程的补充技术。
- 在“
AOP 解决紧密耦合的难题”(
developerWorks,2004
年 2 月)中,Andrew Glover 又回来了,这一次他展示了如何用静态横切来解耦企业应用程序。
- Sean Sullivan 的“
高级 DAO 编程”(
developerWorks,2003
年 10 月)提供了对 DAO 模式的简要介绍,然后直接进入如何在 J2EE 编程中发挥它的强大功能。
- 对于那些刚开始接触 CMP CMR 的人,Richard Hightower 编写了一个关于这个主题的全系列教程,首先是“
Introduction
to container-managed persistence and relationships”(
developerWorks,2003
年 3 月)。
- 有关 XDoclet 的简要教程,请参阅也是由 Richard Hightower 所写的“
Enhance
J2EE component reuse with XDoclet”(
developerWorks,2003
年 5 月)。
- 当然,还可以查看 SourceForge 上的
Hibernate
XDoclet 标签。
- 在
developerWorks
Java
技术专区中可以找到关于 Java 编程各个方面的文章。
- 访问
Developer
Bookstore,获得技术书籍的完整列表,其中包括数百本 Java 相关主题的书籍。
- 还可以参阅
Java
技术专区教程页,获得
developerWorks
免费 Java 教程的完整清单。
Richard 在 Arc-Mind Inc. 工作,在那里他主要从事 Struts 和 J2EE 的技术指导和咨询。Rick 是一位天生的软件开发人员,已有 14 年软件开发经验(其中 7 年是在 Java 平台上)。Rick 编写过几本 Java 技术书籍,并为 Java Developer's Journal和 IBM developerWorks 撰写文章。Rick 最近在 SourceBeat 上完成了一本关于 Struts 的书籍。