9.2 Hibernate持久层
采用Hibernate作为持久层技术的最大的好处在于:可以完全以面向对象的方式进行系统分析、系统设计。面向对象的分析和面向对象的设计才最接近于程序员的自然思维。Hibernate的功能十分强大,对于Hibernate的介绍,在这里只仅介绍与本节内容相关的技术,如需了解Hibernate的详细情况,请参考Hibernate的官方文档和相关书籍。
9.2.1 编写PO类
下面介绍系统的类图,在开发过程中可以根据类图,生成关系型数据库表;或者先把数据库的业务表设计好,再通过工具生成对象。有很多设计工具都可以实现以上功能,如PowerDesigner等。但从面向对象分析与设计来讲,推荐使用第一种方法,因为更贴近面向对象的思想。映射配置文件写好以后,我们也可以用Hibernate生成数据库的表。
图9.1显示了本系统PO的类图。

图9.1 系统PO的类图
从类图可以看出,根据要实现的功能,系统的模型Model实现类有四个:Category, News, NewsReview和User,它们均继承父类BaseObject,都是普通的JavaBean,下面是四个基本的 Persisent Object类。
News:封装了一条消息。包括标题、内容、发布时间及发布人等。
Category:封装了一个消息分类。
User:封装了一个用户的信息。
NewsReview:封装了一条消息评论。
其中,News有一个Category类型的成员变量及User类型的成员变量;NewsReview有一个News类型的成员变量和一个User类型的成员变量。为了能够使用双向关联(Hibernete的映射功能,稍后解释),Category有一个集合型成员变量,用于存放与这个Category对象有关联的News对象;同样News也有一个集合型成员变量,用于存放与这个News对象有关联的NewsReview对象。
实际上,持久化就是通过成员变量来映射关系数据库里的1-N对多和N-N的关系。
下面是PO父类BaseObject的代码:
//将父类声明为abstract类
public abstract class BaseObject implements Serializable
{
//PO推荐实现的toString方法
public abstract String toString();
//PO推荐实现的equals方法
public abstract boolean equals(Object o);
//PO推荐实现的hashCode方法
public abstract int hashCode();
}
父类BaseObject是一个抽象类,定义了三个抽象方法toString(),equals() 和hashCode(),这三个方法是Hibernate推荐持久化对象时重写的。
关系数据库里的记录可以由主键来唯一标识,但是用什么标准来标记两个对象“相等”呢?两个对象相等与否的判断结果很可能影响到数据的完整性。如果在Hibernate无法自行制定两个对象相等与否的标准时,则需要用户自行定义——即重写equals()方法。
如果希望将持久化对象放进Set里,或者重新接管已脱管(detached)的持久化对象,则必须重写equals() 和hashCode()这两个方法,因为Java语法要求如果两个对象相等,那么它们的hashCode()返回值必须相等,因此该方法也需要重写。另外,toString()方法用于给出该对象的描述信息,因此也推荐重写该方法(Hibernate并没有硬性规定)。此外,BaseObject还实现了Serializable接口,该接口是持久化对象推荐实现的。
让其他类继承这个抽象类只是一个可选的写法,至少直接让其他类重写equals()和hashCode()方法来实现Serializable接口也是可以的。
下面具体来看一下News类。
/**
* @hibernate.class table="news"
* @struts.form include-all="false" extends="BaseForm"
*/
public class News extends BaseObject
{
//标识属性
private Long id;
//消息标题
private String title;
//消息内容
private String content;
//用于关联发布人,对应另一个持久化类
private User poster;
//发布日期
private Date postDate;
//最后一次回复日期
private Date lastModifyDate;
//消息分类,用于关联另一个持久化类
private Category category;
//用于关联消息回复
private Set newsReviews;
//无参数的构造器
public News()
{
}
/**
* 用于获取与此消息相关的全部回复
* @return 返回消息的全部回复
*/
public Set getNewsReviews()
{
return newsReviews;
}
/**
* 设置消息关联的回复
* @param newsReview 消息关联的全部回复
*/
public void setNewsReviews(Set newsReviews)
{
this.newsReviews = newsReviews;
}
/**
* 返回消息所属的种类
* @return 消息所属的种类
* @hibernate.many-to-one column="id" not-null="true"
*/
public Category getCategory()
{
return category;
}
/**
* 设置消息所在的种类
* @param 消息所属的种类
*/
public void setCategory(Category category)
{
this.category = category;
}
/**
* 返回消息的最后评论日期
* @return 消息的最后评论日期
* @hibernate.property column="last_modify_date" not-null="true"
*/
public Date getLastModifyDate()
{
return lastModifyDate;
}
/**
* 设置消息的最后评论时间
* @param 消息的最后评论日期
*/
public void setLastModifyDate(Date lastModifyDate)
{
this.lastModifyDate = lastModifyDate;
}
/**
* 返回消息的发布日期
* @return 消息的发布日期
* @hibernate.property column="post_date" not-null="true"
*/
public Date getPostDate()
{
return postDate;
}
/**
* 设置消息的发布日期
* @param 消息的发布日期
*/
public void setPostDate(Date postDate)
{
this.postDate = postDate;
}
/**
* 返回消息的发布者
* @return 消息的发布者
* @hibernate.many-to-one column="username" not-null="true"
*/
public User getPoster()
{
return poster;
}
/**
* 设置消息的发布者
* @param 消息的发布者
* @hibernate.many-to-one column="username" not-null="true"
*/
public void setPoster(User poster)
{
this.poster = poster;
}
/**
* 返回消息的内容
* @return 消息的内容
* @hibernate.property column="content" length="3000" not-null="true"
*/
public String getContent()
{
return content;
}
/**
* 设置消息的内容
* @param 消息的内容
*/
public void setContent(String content)
{
this.content = content;
}
/**
* 返回消息的id
* @return 消息的id
* @hibernate.id column="id" generator-class="increment"
* unsaved-value="null"
*/
public Long getId()
{
return id;
}
/**
* 设置消息的id
* @param 消息的id
*/
public void setId(Long id)
{
this.id = id;
}
/**
* 返回消息的标题
* @return 消息的标题
* @hibernate.property column="title" length="50" not-null="true"
*/
public String getTitle()
{
return title;
}
/**
* 设置消息的标题
* @return 消息的标题
*/
public void setTitle(String title)
{
this.title = title;
}
/**
* 实现抽象父类BaseObject的equals的方法
* @see java.lang.Object#equals(Object)
*/
public boolean equals(Object object)
{
if (!(object instanceof News))
{
return false;
}
News rhs = (News) object;
return this.poster.equals(rhs.getPoster())
&& this.postDate.equals(rhs.getPostDate());
}
/**
* 实现抽象父类BaseObject的hashCode的方法
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
return this.poster.hashCode() + this.postDate.hashCode();
}
/**
* 实现抽象父类BaseObject的hashCode的方法
* @see java.lang.Object#toString()
*/
public String toString()
{
return new ToStringBuilder(this).append("id", this.id).append("title",
this.title).append("postDate", this.postDate).append("content",
this.content).append("lastModifyDate", this.lastModifyDate)
.append("poster", this.poster)
.append("category", this.category).append("newsReviews",
this.newsReviews).toString();
}
}
Hibernate对PO几乎不作任何要求,一般的POJO(Plain Old Java Object)就可以充当PO。只要有一个默认的不带参数的构造器就可以。推荐实现Serializable接口时,最好重写equals()和hashCode()方法。
所谓的POJO就是只有属性,以及属性对应getter和setter方法的Java对象。
下面将解释各个属性的含义(后面的映射配置文件会有进一步探讨):每个News对象就相当于数据库表里的一条记录,其中id属性映射的是记录的主键;title与content分别是消息的标题和内容;postDate是发布时间;lastModifyDate是最后评论时间;category则是News关联(在数据库里通过外键关联)的Category对象;newsReviews则是News对象关联的所有NewsReview对象。
细心的读者会发现,上面的代码重写equals()和hashCode()方法时,并不是采用id作为判断这两个对象相等的标准。这与Hibernate的机制有关,因为对象的标识属性值由Hibernate负责生成,Hibernate仅对已经持久化的对象分配标识属性值,未被持久化的对象是没有标识属性值的。如果在Set里面的一个新建对象未被持久化,此时持久化该对象并分配主键。由于equals()和hashCode()方法都基于主键,因此,hashCode得出的结果会改变,在Set里面就可能起冲突。
因此建议使用“业务键”来作为equals()和hashCode()方法的基准。“业务键”指的是能在真实世界里区分两个对象的属性。例如News就采用了发布人(poster)跟发布时间(postDate)的组合作为业务键,因为同一用户不可能在同一时间发布多条消息。关于如何选择业务键的更多信息请参照Hibernate官方文档。
注意:除了由用户自己生成equals()和hashCode()方法外,也有一些工具可以自动生成这两个方法。如Eclipse插件commonclipse。有兴趣的读者可以尝试一下。
9.2.2 编写PO的映射配置文件
仅有一个POJO是无法完成数据库的持久化操作,还必须为POJO增加映射文件。当POJO加映射文件后,可以完成O/R Mapping,从而在某个特定对象的管理下完成数据库访问。
因此必须为这个POJO配上一个映射文件,通常将这个映射文件命名为:类名.hbm. xml,并与这个类放置在同一目录下。News.hbm.xml的具体内容如下:
<?xml version="1.0" encoding="gb2312"?>
<!-- Hibernate映射文件的文件头,包含DTD等信息-->
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!-- 映射News持久化类-->
<class name="org.yeeku.model.News" table="news">
<!-- 映射标识属性-->
<id name="id" column="id" unsaved-value="null">
<!-- 定义主键生成器策略-->
<generator class="increment">
</generator>
</id>
<!-- 映射关联类:Category-->
<many-to-one name="category"
class="org.yeeku.model.Category" column="category_id" not-null="true">
</many-to-one>
<!-- 映射最后评论日期属性-->
<property name="lastModifyDate" column="last_modify_date" not-null="true">
</property>
<!-- 映射发布日期属性-->
<property name="postDate" column="post_date" not-null="true">
</property>
<!-- 映射关联类:User-->
<many-to-one name="poster" column="username" not-null="true">
</many-to-one>
<!-- 映射消息内容属性-->
<property name="content" column="content" length="3000" not-null="true">
</property>
<!-- 映射消息标题属性-->
<property name="title" column="title" length="50" not-null="true">
</property>
<!-- 映射关联类:NewsReview,映射1-N关联-->
<set name="newsReviews" lazy="false" inverse="true" cascade="all-
delete-orphan">
<meta attribute="field-description">
@hibernate.list lazy="true" inverse="false" cascade="none"
@hibernate.collection-key column="id"
@hibernate.collection-one-to-many class="org.yeeku.model.
NewsReview"
</meta>
<key>
<column name="news_id" />
</key>
<one-to-many class="org.yeeku.model.NewsReview" />
</set>
</class>
</hibernate-mapping>
映射文件根元素hibernate-mapping下面可以有多个class子元素,即在一个映射文件里可以配置多个映射对象,但为了清晰起见,建议为每个类单独写一个映射配置文件。
class的name属性就是要映射的对象类;table是数据库里对应的表名;class下面的子元素就是这个类的属性并与数据库里的字段相对应。
id用于唯一标识该对象,称为标识属性。其中name属性是类里面的属性名;column是数据库的对应字段。另外,id还有generator子元素,用于指定主键生成方式,这里用的是自动增长生成(increment)策略,其他方式请参照Hibernate文档。
这里重点介绍1-N(set是其中一种方式)和N-1(many-to-one,通常就是外键关联)的映射。首先来看1-N对多关系,一条消息对应多条评论。name属性是指类里面存放“N”的一方对象的属性。meta一栏这里只是用作为阅读者提供额外信息。key就是“多”的一方存放“N”的一方外键的字段,one-to-many的class是“N”的一方的映射对象。
注意:lazy属性与inverse属性的设置。lazy是Hibernate的一种延迟加载数据策略,比如说一个News对象与多个NewsReview对象关联,如果使用lazy策略,则只有当NewsReview对象被请求时(例如调用News的getNewsReviews方法)才会从数据库里读取相应记录,从而达到优化性能的目的。特别是系统如果非常复杂,关联很多时,不使用延迟加载将会对性能有比较大的影响。当然使用延迟加载还有很多问题需要注意,这里为了方便起见设为false,有兴趣的读者可以参考Hibernate官方文档。inverse属性标明该元素是否控制关联关系,对于1-N的关系,通常推荐由“N”的一段控制关联关系,因此set元素通常都应包含inverse=“true”属性。
N-1的配置则比较简单,与普通属性的配置一样,依次类推将所有映射文件完成,可以参照src/org/yeeku/model/NewsReview.hbm.xml,Category.hbm.xml和User.hbm.xml。完成后可以通过配置Hibernate的属性(数据库连接驱动,数据库方言,登录用户名跟密码等等)自动生成数据库的表,这里不再赘述。
注意:除了手工编写以外,开发者还有一些工具可以帮助生成映射配置文件。例如Xdoclet就可以用来生成Hibernate和Struts等配置文件,只需在编写源代码的时候在适当的地方加上注释,再用Xdoclet生成即可。
具体例子参看上面的News类源码,类定义上包含如下注释:
@hibernate.class table="news"
标明了该类映射的数据库表是news。所有的getter方法上面的注释里都有类似的注释,用来标明属性映射的字段。例如:
@hibernate.property column="content" length="3000" not-null="true"
等。有兴趣的读者可以对照News.hbm.xml或者直接参考Xdoclet的文档。
9.2.3 连接数据库
除了前面介绍的POJO和映射文件之外,Hibernate还不知道与哪个数据库连接,也不知道连接数据库时需要哪些属性。因此,Hibernate控制数据库连接提供了两种方式:
采用hibernate.properties属性文件。
采用hibernate.cfg.xml配置文件。
这两种方式只是形式上的区别,实质的内容没有任何改变。都需要指定连接数据库的URL、数据库的驱动、用户名及密码等基本信息。如果需要使用连接池,则还应确定连接池配置信息。
1.直接Hibernate的配置连接
直接Hibernate的配置连接就是使用hibernate.cfg.xml文件,关于使用hibernate. properties的方式与此类似,不再赘述。
下面是使用C3P0连接池的hibernate.cfg.xml配置文件的源代码:
<?xml version="1.0" encoding="GBK"?>
<!-- Hibernate配置文件的文件头,包含DTD等信息-->
<!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<!-- Hibernte配置文件的根元素-->
<hibernate-configuration>
<!-- 配置SessionFactory-->
<session-factory>
<!-- 配置连接数据库的URL-->
<property name="connection.url">jdbc:mysql://localhost/j2ee</
property>
<!-- 配置连接数据库的用户名-->
<property name="connection.username">root</property>
<!-- 配置连接数据库的密码-->
<property name="connection.password">32147</property>
<!-- 配置连接数据库的驱动-->
<property name="connection.driver_class">com.mysql.jdbc.Driver</
property>
<!-- 配置连接数据库的方言-->
<property name="dialect">org.hibernate.dialect.MySQLDialect</
property>
<!—JDBC连接池(use the C3P0) -->
<!-- 连接池的最大连接数-->
<property name="hibernate.c3p0.max_size">500</property>
<!-- 连接池的最小连接数-->
<property name="hibernate.c3p0.min_size">2</property>
<!-- 连接池的超时时长-->
<property name="hibernate.c3p0.timeout">5000</property>
<!-- 连接池的缓存statement的数量-->
<property name="hibernate.c3p0.max_statements">100</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>
<!-- 连接池每次获取连接的数量-->
<property name="hibernate.c3p0.acquire_increment">2</property>
<property name="hibernate.c3p0.validate">true</property>
<!-- 每次执行持久化操作,是否显示对应的SQL语句-->
<property name="hibernate.show_sql">true</property>
<!-- 是否在启动时,重新创建数据库 -->
<property name="hbm2ddl.auto">update</property>
<!-- 此处列出所有的映射文件-->
<mapping resource="Person.hbm.xml"/>
</session-factory>
</hibernate-configuration>
Hibernate通过上面的配置文件,知道如何控制与数据库的连接,以及采用连接池的方式。
2.采用Spring管理Hibernate
让Spring管理Hibernate时,必须将Hibernate的SessionFactory配置在Spring容器内,此时有两种做法:
在Spring容器中配置SessionFactory。
让hibernate.cfg.xml文件控制SessionFactory。
这两种做法的效果相似,都是利用Spring的LocalSessionFactoryBean,负责产生Hibernate的SessionFactory,该bean作为其他持久化访问组件的属性注入。
下面是完成在Spring容器配置Hibernate的SessionFactory的形式,该项目采用以下方式:
<beans>
<!-- 配置数据源,使用DBCP数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<!-- MySQL数据库的驱动-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!-- 数据库的URL-->
<property name="url" value="jdbc:mysql:///newsboard"/>
<!-- 指定数据库的用户名-->
<property name="username" value="root"/>
<!-- 指定数据库的密码-->
<property name="password" value="123"/>
<!-- 指定数据库的最大连接数-->
<property name="maxActive" value="100"/>
<!-- 指定数据库的最大空闲连接数-->
<property name="maxIdle" value="30"/>
<!-- 指定数据库的最大等待数-->
<property name="maxWait" value="1000"/>
<!-- 指定数据库的默认自动提交-->
<property name="defaultAutoCommit" value="true"/>
<!-- 指定数据库的连接超时时是否启动删除-->
<property name="removeAbandoned" value="true"/>
<!-- 指定数据库的删除数据库连接的超时时长-->
<property name="removeAbandonedTimeout" value="60"/>
<property name="logAbandoned" value="true"/>
</bean>
</beans>
本系统使用的数据源是DBCP的数据源,DBCP数据源是来自Apache组织的一个优秀的数据源。
DBCP数据源有四个属性值得注意。
driverClassName:数据库驱动,通常由第三方提供。
url:数据库URL,不同数据库的写法存在差异。
username:数据库的用户名。
password:数据库的密码。
配置不同的数据库时,这四个属性通常需要改动。driverClassName由数据库厂商提供,url的写法也由厂商决定,请参照数据库的文档。这里的数据源使用的是MySQL,连接的是newsboard数据库。
另外,Spring提供了对Hibernate的SessionFactory的管理,只需在application Context. xml文件中增加如下配置:
<!-- 配置Hibernate的SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.
LocalSessionFactoryBean">
<!-- 依赖注入SessionFactory所需的DataSource-->
<property name="dataSource" ref="dataSource"/>
<!-- 加载所有的映射文件-->
<property name="mappingResources">
<!-- 下面列出所有的持久化映射文件-->
<list>
<value>org/yeeku/model/User.hbm.xml</value>
<value>org/yeeku/model/News.hbm.xml</value>
<value>org/yeeku/model/NewsReview.hbm.xml</value>
<value>org/yeeku/model/Category.hbm.xml</value>
</list>
</property>
<!-- 下面指定Hibernate的属性-->
<property name="hibernateProperties">
<props>
<!-- 下面指定Hibernate使用的数据库方法-->
<prop key="hibernate.dialect">org.hibernate.dialect.
MySQLDBDialect</prop>
</props>
</property>
</bean>
其中<property name="dataSource" ref="dataSource"/>就是我们刚才配置的数据源。mappingResources 是我们编写好的映射配置文件。除此之外还需要在hibernateProperties里配置Hibernate访问数据库使用的方法,到此为止,已经基本完成了Spring与Hibernate的整合。
如果需要让hibernate.cfg.xml文件自己负责Hibernate SessionFactory的配置,可以采用如下方式:同样使用LocalSessionFactoryBean生成SessionFactory,但此时无须确定数据库用户名及密码等具体信息,只需在配置文件中确定hibernate.cfg.xml文件的位置。
配置代码如下:
<!-- 配置Hibernate的SessionFactory-->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.
LocalSessionFactoryBean">
<!-- 确定配置文件的位置-->
<property name="configLocation">
<value>classpath:hibernate.cfg.xml</value>
</property>
</bean>






