16.2 持久层设计

虽然本章介绍的重点是Web Service,但也会详细讲解如何将传统Java EE应用升级为Web Service,因此本章将会逐一介绍应用中的各个实体的功能。本节将从系统功能出发,提取系统实体,从而完成本系统的持久层设计。

16.2.1 系统实体

本章所介绍的系统是一个拍卖系统,所有的用户必须先登录系统才可使用该系统的功能,而且系统中所有功能都是由用户驱动的,因此本系统必须有一个实体——拍卖用户。

用户登录系统后,可以添加自己的拍卖物品,也可以浏览其他人正在拍卖的物品,还可以浏览自己已经赢取的物品。用户与物品的用例图如图16.2所示。

alt

图16.2 用户与物品的用例图

从图16.2中不难看出,本系统中必然包括一个物品实体,该物品实体就代表了系统中正在拍卖的物品。用户可以添加物品,此时用户和物品之间存在所属关系;用户也可以赢取物品,用户和物品之间存在赢取关系。可见,用户和物品之间存在两种一对多的关联关系。

除此之外,用户还可以对物品竞价,每个物品的历史竞价记录也应该被保存下来,这就意味着系统中包含了一个竞价实体,登录用户可以添加竞价(对某个物品进行竞价),也可以浏览自己的竞价记录。用户和竞价实体之间有图16.3所示的用例图。

alt

图16.3 用户与竞价的用例图

从图16.3中不难看出,系统中必须有竞价记录实体。用户可以添加竞价记录,可以添加多条竞价记录,可见用户和竞价记录之间存在一对多的关联关系。除此之外,用户添加竞价记录时,必须对指定物品添加竞价记录,同一物品可以有多个用户参与竞价,这意味着物品和竞价记录之间存在一对多的关联关系。

拍卖系统中物品是关键,但物品必须提供一个简单的分类,要对物品进行有效地分类管理,则系统还应该增加一个物品种类实体;当物品进入拍卖系统后,可能存在拍卖中、流拍和被成功赢取3种状态,但考虑到系统的开放性,故将物品状态也保存到系统中,这就意味着系统中应该增加物品状态实体。

综上所述,本系统中应该包含5个实体:物品、用户、竞价记录、物品状态和物品种类。

16.2.2 系统E-R图和数据表

经过上面的介绍,不难画出该系统的E-R(实体-关系)图,这种图对于分析系统实体、确定实体关系有非常大的好处。本系统的E-R图如图16.4所示。

alt

图16.4 电子拍卖系统E-R图

从图16.4中可以看出该系统的数据库设计:系统中应该包含哪些数据表,每个数据表里应该包含哪些字段;各表之间应该以怎样的关联关系进行关联(主、外键约束关系)。

对于系统中的物品状态而言,它仅和物品存在一对多关联关系,这种一对多的关联关系,可以通过外键约束来实现,而外键是保存在多的一端的数据表里的。故物品状态的数据表非常简单,仅需要两个字段:状态ID和状态名。图16.5所示是物品状态表的结构。

alt

图16.5 物品状态表的结构

与物品状态实体相似的是,系统中的物品种类也只和物品存在一对多的关联关系,这种关联关系也是通过在物品中保存外键来进行约束的。而物品种类表中只需保存物品种类名和种类描述两个字段。图16.6显示了物品种类表的结构。

alt

图16.6 物品种类表的结构

系统中的用户实体和物品实体、竞价实体都存在一对多的关联关系,甚至和物品之间存在两种一对多的关联关系(一种是所属,一种是赢取),但因为用户都是一的那端,故无须保存外键,只需要保存用户自身的属性即可。图16.7显示了用户表的表结构。

alt

图16.7 系统用户表的结构

系统的核心是拍卖物品,拍卖物品和用户之间存在两种多对一的关联关系,这两种多对一的关联关系都需要在物品表中保存外键。除此之外,拍卖物品还和竞价记录之间存在一对多的关联关系,但这种关联关系通过在竞价记录表中保留外键实现,故拍卖物品表里只需保存物品属性,以及两个与拍卖用户关联的外键。拍卖物品还和物品种类实体、物品状态实体之间存在多对一的关联关系,这种关联关系是通过在物品表中增加外键来实现的。

图16.8显示了拍卖物品表的表结构。

alt

图16.8 拍卖物品表的结构

系统的竞价记录实体则和拍卖物品、系统用户存在多对一的关联关系,故需要在竞价记录表中增加两个外键。图16.9显示了系统的竞价记录表的表结构。

alt

图16.9 竞价记录表的结构

有了上面的表结构,就可以大致了解系统中PO(持久化对象)的类结构了。对于Java EE应用而言,虽然通常采用的是面向对象分析、面向对象设计,但数据库设计对于系统功能的实现也同样重要,因此数据库设计也可以用来辅助系统的持久层PO设计。

16.2.3 实现Hibernate PO

Hibernate PO由两个部分组成:持久化类和映射文件,其中映射文件负责把持久化类映射到数据表,并把持久化类的属性映射到数据列。通过这种映射,允许程序以面向对象的方式操作Hibernate的PO,而Hibernate则负责把这种操作转换成底层的JDBC数据库访问。

对于Hibernate PO而言,通常一个持久化类被映射到一个数据表,而持久化类的属性则对应到数据表的数据列,因此系统实体表里包含了多少列,就意味着该实体对应的持久化类应包含多少个属性。

下面是系统中物品状态PO类的代码:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\State.java

alt

提供了该PO代码后,还应该为该PO提供对应的映射文件,该PO对应的映射文件代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\State.hbm.xml

alt

从上面的映射文件不难看出,映射文件主要用于把PO实体映射到数据表,上面的映射文件将State实体映射到了state数据表。

映射文件的根元素是<class…/>元素,每个<class…/>元素用于映射一个持久化类,在<class…/>元素中指定的table属性用于表明该持久化类所映射的数据表。如果没有指定table属性,即该数据表的表名与持久化类的类名相同。

<class…/>元素下包含了多个<property…/>子元素,每个<property…/>子元素用于映射一个持久化属性,即完成一个属性和一个数据字段之间的映射。

与之类似的是,系统中还包含物品种类实体,该实体对应的PO类代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Kind.java

alt

该物品种类PO对应的映射文件如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Kind.hbm.xml

alt

上面的映射文件用于将持久化类Kind映射到系统中的kind数据表。

系统中的拍卖用户实体对应的PO类代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\AuctionUser.java

alt

从上面的PO类中可以看出,PO类采用Set类型来保存该实体一对多的关联实体,这是符合Hibernate的关联映射策略的。

当PO类中采用了Set属性来保存所有关联实体后,就可以在映射文件中使用<set …/>元素来映射这种关联实体。

下面是实体映射文件的代码:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\AuctionUser.hbm.xml

alt

上面的映射文件将AuctionUser PO类映射到了auction_user数据表,并使用了一系列的<set …/>元素来映射该实体所对应的关联实体。

在上面的关联映射中,存在大量的一对多关联映射,对于这种一对多的关联映射,通常推荐做成双向关联映射。双向的一对多关联映射,有最好的性能表现。

上面的AuctionUser实体与Bid实体存在一对多的关联关系,且前面的物品种类、物品状态和拍卖物品之间也存在一对多的关联关系。Hibernate完全可以理解这种一对多的关联关系,也可以很好地支持这种一对多的关联关系,但建议避免使用一的那端来控制关系。


alt注意

推荐将一对多的关联映射定义成双向一对多关联,并且避免通过一的那端来控制关联关系,这样可以有较好的性能表现。因此,上面的映射文件中所有的<set…/>元素都增加了inverse="true"属性。


对Hibernate而言,完全支持将普通的POJO映射成PO,但这些POJO应尽量遵守如下规则:

alt 提供一个默认的(无参数的)构造器实现。

alt 提供一个标识属性(identifier property),用于标识该实例。

alt 使用非final的类。尽量避免将POJO声明成final的,这将导致性能的下降。

alt 如果需要将某个实体放入Set 中,则应该重写equals和hashCode两个方法,这是因为Set集合根据这两个方法的返回值来判断两个实体是否相同。


alt注意

重写hashCode和equals方法时,不应该根据实体的标识属性,因为该标识属性实际是该记录的逻辑主键值;而应该根据实体的逻辑标识属性。如果系统有一个需求:两个人的身份证号相同,就可判断两个人相同。那么重写hashCode方法和equals方法时,就应该根据身份证号来判断,而根据标识属性重写这两个方法没有意义:数据库生成的两个逻辑主键肯定不同,这将导致相同的人在数据库里生成两条记录。


因为系统中的Bid实体需要保存到AuctionUser实体的Set属性中,也需要保存到Item实体的Set属性中,故应该重写hashCode和equals方法。

系统认为两次竞价记录是同一条记录的根据是:如果两次竞价记录的用户相同,竞价物品相同,竞价金额也相同,则两次竞价记录是相同的。故该竞价记录实体的代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Bid.java

alt

该实体PO对应的映射文件代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Bid.hbm.xml

alt

在上面的映射文件中包含了多个<many-to-one…/>子元素,每个<many-to-one…/>子元素用于映射一个多对一关联实体,映射时增加了lazy="false"属性,表明该关联实体关闭延迟加载。但对于映射文件中的<set…/>元素,则不可以关闭延迟加载。


alt注意

对于多对一的关联,可以关闭延迟加载,因为多个实体只对应一个实体。关闭延迟加载并不会引起太多的性能下降。但对于一对多的关联,则不应该关闭延迟加载。如果关闭延迟加载,则可能在抓取一条主记录时有百万条从记录随之初始化——这对性能的消耗非常大。


对于系统的拍卖物品而言,只要用户添加新物品,即使系统中已有一个与之同名的物品,我们也不能断定该物品和原有物品就是相同物品——因为用户可能有多个相同物品需要参加拍卖,如果我们不需要根据逻辑属性来判断两个物品是否相同,则无须重写equals和hashCode方法,故下面的Item PO类没有重写这两个方法。该PO类的代码如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Item.java

alt

该PO类对应的映射文件如下:

程序清单:codes\16\auction\WEB-INF\src\org\crazyit\auction\model\Item.hbm.xml

alt

实现了系统的PO后,程序就可以通过对PO的创建、修改和删除等操作,转换成对底层数据库记录的新增、修改和删除等操作。但Hibernate还不知道操作哪个数据库,不知道数据库的连接信息,为了指定数据库连接信息,必须管理Hibernate的SessionFactory实例,Hibernate通过SessionFactory来包装底层数据库连接。

16.2.4 管理SessionFactory

前面已经指出,Hibernate映射文件仅仅完成了持久化类和数据表之间的映射,而依然无法准确知道要操作的数据库。对操作数据库的定义是通过SessionFactory来管理的,SessionFactory是对数据库连接的封装,而Hibernate的持久化操作就是建立在SessionFactory的基础上的。因此,每个使用Hibernate作为持久层解决方案的项目中,对SessionFactory的管理都是一个非常重要的部分。

Spring的IoC容器是一个功能非常强大的工厂,它不仅可以管理系统中的业务逻辑组件,也可以管理系统中的数据源,还可以管理Hibernate的SessionFactory实例。一旦我们将SessionFactory配置成一个普通Bean,当Spring容器启动时,系统就将自动创建SessionFactory Bean。

由于Spring的IoC容器将SessionFactory纳入了容器的管理之中,而且系统的DAO组件也在容器的管理之中。因此,Spring的IoC容器还可将SessionFactory实例注入到每个需要持久化操作的组件中,从而以松耦合方式来管理Hibernate的SessionFactory,避免了直接在DAO组件中主动获取SessionFactory实例。

在Spring容器中配置SessionFactory的配置片段如下:

程序清单:codes\16\auction\WEB-INF\applicationContext.xml

alt

一旦在Spring容器中配置了该SessionFactory Bean,Spring容器就将负责创建并管理该SessionFactory Bean,并可充分利用Spring IoC容器优势,将SessionFactory Bean注入给其他DAO组件。