2006年8月1日星期二

Hibernate Jpetstore 之二 数据层技术

文档内容

  • 概览
    • 面象对象与结构化的错配
    • Java 代码:面向对象的表示
    • 数据库表:结构化的数据表示

  • DAO调用序列图
  • Strtus <-- Spring --> DAO (Spring 作为 DAO 与Struts 的桥梁)
  • 实体关系图
  • 实体关系映射(O/R Mapping)
    • Java代码中的关系表示
      • 单向关系
      • 双向关系
    • Hibernate 的关系表示
      • account - category
      • account - order
    • 主键映射
    • 其他映射
    • 二级缓存映射
    • Hibernate 配置文件
    • Ehcache

  • 总结


在阅读本篇文章之前,请先仔细阅读 Hibernate Jpetstore之一:数据层 的相关内容。


此篇文章中或多或少会重复一些系列之一的相关内容,放在这里以达到理解的连续性。
再有,在讨论时,我不时地会“跑题”,这也是为了将大量的信息转达出来,因为象这种书面形式的机会实在难得。

概览


一般来说,数据层设计的好坏直接关系到整个系统的成败。在关系数据库占工业主导地位的今天,不论采用何种数据层技术,必然牵涉到数据库表,以及表与表之间
的关系。值得注意的是,我们面对的项目需求文档,我们的工作(至少是设计者的工作)是从文档中找到“有意义”的名词,对于面向对象的设计方法来说,就是把
这些名词表示为对象;(文档中与该名词有关的约束,则作为对象的属性,而与之相关的动词就是该对象的行为,即函数或方法)

你可能会问,我们是在谈数据层技术,怎么突然转到了面向对象的概念了?


是的,在面向对象编程语言占主导地位,而数据管理与存储技术(即数据库)仍处在“结构化”的今天,我不得不寻求一种折衷的方案,来将程序的面向对象化
据的结构化
间的“错配”的影响减到最小。Hibernate 即是这一方案中的先行者。



前面这个回答虽然简单地道出了问题的所在,并以 Hibernate
作出了回答,但是是否真正明白了呢?我想只有在项目中真正体验过数据层设计后才会悟出这句话的真谛。但我这里还是打算介绍一下这几个词语:面象对
象,结构化及它俩的错配。

面象对象与结构化的错配


需求:用户可以使用多种支付方式付款如信用卡银行转帐 ...
名词:用户, 信用卡,银行转帐
动词:付款


用户可以用 User 来表示,而因为有多种支付方式,而每种支付方式必然存在
着某些共性或相同的动作,因此可以用一个支付详情来作为父类
BillingDetail, 而信用卡可用子类
CreditCard 来表示,银行转帐可以用
BankAccount 来表示。为了使讨论简化,我们现在只考虑父类 BillingDetail。


付款 bill() 方法由子类实现对应的付款功能。

Java 代码:面向对象的表示


public class User {
private String name;
private String sex;
private String address;

// 因为用户可以多种支付方式,因此采用集合类 Set
private Set<BillingDetail> billingDetails;

// Accessor methods (getter/setter), business methods, etc.
...
...
}



public abstract class BillingDetail {
private String accountNumber;
private String accountName;
private String accountType;
// 每个支付详情仅属于一个用户,因为支付信息中有帐户名,帐号之类的信息
private User user;

// Accessor methods (getter/setter), business methods, etc.

...
public abstract void bill();
...
}

数据库表:结构化的数据表示

create table USER (
NAME varchar(15) not null primary key,
SEX varchar(1) not null, -- 'M' Male, 'F' Female
ADDRESS varchar(100)
)

create table BILLING_DETAILS (
ACCOUNT_NUMBER varchar(10) not null primary key,
ACCOUNT_NAME varchar(50) not null,
ACCOUNT_TYPE varchar(2) not null,
NAME varchar(15) foreign key references user
}


注意,从 USER 的 create table 语句根本看不出它与 BILLING_DETAILS 有什么关系,但当我们看到
BILLING_DETAILS 中的外键约束行 NAME
varchar(15) foreign key references user
时,才可以得知 USER 与 BILLING_DETAILS 是相关联的,就上述建表语句来讲,用户与付款详情的关系为一对多关系,表示为为 USER --- (1..*)
---> BILLING_DETAILS



但是如果外键约束为:
NAME unique varchar(15) foreign key references user

则它们之间的关系成了一对一的关系了,USER --- (1..1)
---> BILLING_DETAILS




但是对于面向对象的程序而言,我们一眼就能看出它们之间存在关系,而且我们很快就可以写出下面的代码:


从 User 来找到对应的 BillingDetail

Set billingDetails = aUser.getBillingDetails();

for (Iterator i = billingDetails.iterator(); i.hasNext(); ) {
String accountNumber = (BillingDetail)i.next()).getAccountNumber();
...
}


或,从 BillingDetail 找到对应的 User:

User aUser = billingDetail.getUser();
String address = aUser.getAddress();


对于结构化的数据,我们采用 SQL:

select u.ADDRESS, bd.ACCOUNT_NUMBER from USER u, BILLING_DETAIL bd
where bd.NAME = u.NAME and u.NAME = '悟空';

也许从乍一看,并看不出其中的问题,但是对于象上述这样需要得到两个相关表中的数据(获得用户“悟空”的地址和帐号),因为数据是分布在两个表中,所以必
须将两个表联合 (join, 这个词是出自于标准的SQL: select u.ADDRESS, bd.ACCOUNT_NUMBER from
USER u joint BILLING_DETAIL
bd on bd.NAME = u.NAME where
u.NAME = '悟空'; 这与上面的写法效果是等同的),联合意味着扫描两张表。



而对于面向对象的语言,我们可以轻松地从一个对象访问到另一个对象,只要对象中包含了被访问对象的引用即可。我们也可以轻易地看出它们之间的关系,比如
Use 包含一个集合类型 Set 的 BillingDetail引用,则可知其包含多个 BillingDetail, 而
BillingDetail 只包含单个的 User 引用,因此它仅对应一个 User.
而结构化的形式,如SQL则一定要通过引入附加的关键字才能将这一情况加以区分,如 unique, foriegn key



所谓的面向对象关系型数据库(如:db4o)正在努力地解决结构化的
问题,但由于大部分工业基础已经建立在传统的关系型数据库的模式之
上,因此变革将肯定是缓慢的,有时甚至不大可能。


DAO调用序列图


1. 首先,所有的DAO是在 Spring 的应用上下文加载时初始化的。

在 dataAccessContext-hibernate.xml 中有:

  <!-- 每个DAO的实现都需要 Hibernate 的 sessionFactory 属性,
该对象相当于普通的JDBC的连接对象,因此其是用来与数据库进行会话的 -->

<bean id="accountDao" class="org.springframework.samples.
jpetstore.dao.hibernate.HibernateAccountDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="categoryDao" class="org.springframework.samples.
jpetstore.dao.hibernate.HibernateCategoryDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="productDao" class="org.springframework.samples.
jpetstore.dao.hibernate.HibernateProductDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="itemDao" class="org.springframework.samples.
jpetstore.dao.hibernate.HibernateItemDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="orderDao" class="org.springframework.samples.
jpetstore.dao.hibernate.HibernateOrderDao">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>


2. 客户端是通过 Facade 接口来与DAO进行交互的。


在 applicationContext.xml 中有:

<!-- 结合类 PetStoreImpl 中的 setXXXDao 方法,这种方式在 AOP
的术语中叫做基于 setter 的依赖注入;

还有一种是基于构造器的依赖注入,即将所有的依赖关系在构造器中一并初始化好. -->
<bean id="petStore" class="org.springframework.samples.
jpetstore.domain.logic.PetStoreImpl">
<property name="accountDao" ref="accountDao"/>
<property name="categoryDao" ref="categoryDao"/>
<property name="productDao" ref="productDao"/>
<property name="itemDao" ref="itemDao"/>
<property name="orderDao" ref="orderDao"/>
</bean>


3. 而 PetStoreImpl 中有:

public class PetStoreImpl implements PetStoreFacade, OrderService {

// 对应于上面配置文件中的列出的所有属性(property)
private AccountDao accountDao;

private CategoryDao categoryDao;

private ProductDao productDao;

private ItemDao itemDao;

private OrderDao orderDao;

//-------------------------------------------------------------------------
// 基于 setter 的依赖注入方式 (Setter methods for dependency injection)
//-------------------------------------------------------------------------

public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}

public void setCategoryDao(CategoryDao categoryDao) {
this.categoryDao = categoryDao;
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}

public void setItemDao(ItemDao itemDao) {
this.itemDao = itemDao;
}

public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}



这样,就将初始化出来的所有DAO对象"注入" (通过 setXXXDao(XXXDao) 方法) 到 Facade 的实现类
PetStoreImpl

4. 好,我们现在来看看在这个应用中有几个地方使用这个门面BEAN来进行DAO操作。

首先可以肯定的是,有了 PetStoreFacade 这个接口(及其实现类
PetStoreImpl),永远不出现直接初始化某个DAO的代码,如:

AccountDao accountDao = new HibernateAccountDao();

如果不是这样的话,我们前面这几步 1, 2, 3 就白做了,一个个来看:

1. 在 dataAccessContext-hibernate.xml 配置这些DAO干什么? 为了在应用 加载/部署
时就把它们初始化出来。我们只需要保证每个配置 bean 有默认构造器。(我们看看包org.springframework.samples.jpetstore.dao.hibernate 
所有的
HibernateXXXDao就可以看到,它们都没有定义任何构造器,那么则意味着编译器会自动产生一个默认构造器 -- 无参数的 public
构造器,这正好符合要求)


2. 在 applicationContext.xml 把这些初始化好的DAO注入到门面接口PetStoreFacade的实现类
PetStoreImpl干什么?因为这个门面,在门里边汇总了所有的DAO,既然已经都初始化出来了,把它们注入进来是很简单的(采用了
Setter 注入方式)


3. 采用 Setter 注入方式 把配置中的DAO注入进来。



Strtus <-- Spring

--> DAO (Spring 作为 DAO 与 Struts 的桥梁)


因为我们知道,在写程序时最好是使用接口进行类型声明,如:

Map map = new HashMap();

这样,在以后有更好的实现时,切换到其它实现后,调用端不需要改变,因这个类型是 Map接口, 不是实现类
HashMap。但有一种情况例外,如果你的代码中这样:



((HashMap)map).XXX;


这样的话,那么从一种实现平滑切换到接口的另一种实现是不可能的。但是对于 HashMap 是不需要这样的,因为HashMap 所暴露的方法和
Map 是一样的,也就是说 HashMap
没有增加任何方法,所以强制转型是没有意义的,从易扩展(考虑到可能切换到另一种实现)的角度来看,这反而是自己给自己找麻烦。

所以在我们自己的类设计过程中,这些都可以借鉴的。


在 NetBeans 中, 选中 PetStoreFacade ,右击 | Find Usages, 在弹出窗口中选择 'Next' :

在底下列出了所有使用到PetStoreFacade的地方:

1.排除 以 spring 结尾的包(因为那是 SpringMVC 用到的),

2.以logic
结尾的包中除一个是在实现类PetStoreImpl的声明之外,还有一个是SendOrderConfirmationEmailAdvice
中有到了, 但这个是采用 Spring 的面象方面 (AOP) 的拦截机,先忽略它。

3.以 struts 结尾的包正是我们所关心的,我们看到,只有一个类 BaseAction 中用到了这个门面接口。


乍一想,有点好奇,但当我们深入到以 struts 结尾的这个包中时,我们发现,所有的 Action
(即动作,对应页面的提交按钮,链接等)都是从这个 BaseAction 派生的。

再有,我们这篇文章的主题是数据访问层技术,所以作为数据层的调用者,只能是控制器层组件,不可能是数据层内自己的对象,更不可能是表示层组件,如
Struts的 FormBean。

所以这是一个完美的设计。


我们看一下这样关键的类的代码,代码很简单:



/**
Superclass for Struts actions in JPetStore's web tier.
*
* <p>Looks up the Spring WebApplicationContext via the ServletContext
* and obtains the PetStoreFacade implementation from it, making it
* available to subclasses via a protected getter method.
* <p>As alternative to such a base class, consider using Spring's
* ActionSupport class for Struts, which pre-implements
* WebApplicationContext lookup in a generic fashion.
*
* @author Juergen Hoeller
* @since 30.11.2003
* @see #getPetStore
* @see org.springframework.web.context.support.WebApplicationContextUtils#getRequiredWebApplicationContext
* @see org.springframework.web.struts.ActionSupport
*/
public abstract class BaseAction extends Action {

private PetStoreFacade petStore;

public void setServlet(ActionServlet actionServlet) {
super.setServlet(actionServlet);
if (actionServlet != null) {
// fixed by pprun, must be synchronized
synchronized (this) {
ServletContext servletContext = actionServlet.getServletContext();
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
this.petStore = (PetStoreFacade) wac.getBean("petStore");
}
}
}

protected PetStoreFacade getPetStore() {
return petStore;
}

}




1. 首先我们从Struts的 Action 派生下来,是为了 override setServlet
方法,super.setServlet(actionServlet) 这一句是没有被覆盖时的默认动作,即,将 web.xml 中配置的
ActionServlet 引用进来,这样所有的 Action 都可引用这个 ActionServlet, 如果需要的话。



2. 但我最主要的目的不是为了重复 1. 这个根本没有必要的代码,而是为了将门面接口的实现类放到这个 BaseAction 中,这样,所有的
BaseAction 子类,就可以直接 getPetStore()
获得门面了,有了门面了,所有的DAO及DAO的方法都是通过这个门面来进行调用的。
就是门面这个词最好的解释。


3. 我们来看看怎么得到在 applicationContext.xml 配置的门面的实现类:

<bean id="petStore" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">

这个配置告诉我们这个实现类的 Bean id 为 petStore, class 为门面的实现类 PetStoreImpl.

        ServletContext servletContext = actionServlet.getServletContext();
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
this.petStore = (PetStoreFacade) wac.getBean("petStore");

正如我经常强调的,所有的 Java Web 框架都离不开 Servlet, Spring与WEB相关的组件也不例外,它需要
ServletContext 来初始化它的专有名词 WebApplicationContext,

但只要得到了 servletContext, 初始化工作很容易,利用 Spring 提供的工具类
WebApplicationContextUtils 的 getRequiredWebApplictionContext 方法,将得到的
servletContext 传进去即可。

那么怎样才能得到 servletContext 呢?如果对 Servlet 熟悉的话,任何 Servlet- abcServlet都有
abcServlet.getServletContext()方法,它其实是
abcServlet.getServletConfig().getServletContext() 的简化版。


再看看,

super.setServlet(actionServlet);


所以只需从 actionServlet.getServletContext(); 然后传入到Spring中,这样 Spring 就成了 DAO
与 Struts 的桥梁了:

        this.petStore = (PetStoreFacade) wac.getBean("petStore");

Spring, 关键的东西就是它的配置文件,在程序中表示为接口: ApplicationContext,
对WEB应用就是其子接口:WebApplicationContext。 一旦Sping的这个(些)上下文后,所有在配置文件中的东西,在
Spring 都是 Bean,得到它们是很容易的。因为所有的配置的 Bean 都有一个 id, 并指定了其的类型 class,
且正如我们前面所说,要符合 Bean, 必须需要
默认构造器(如果你定义了构造器的话,那么你不得不要写一个默认构造器,因为这时编译器不再为你生成它了,但如果你一个构造器都没指定的话,可以让编译器
Javac 来为你生成,采用什么策略取决于你,但为了清晰起见,最好任何可初始化的类都给出默认构造器,这也是IDE为什么每次 new
一个类时,它都会为你生成的原因)



getBean 的参数即是你所想得到的 Bean 的 id, 在你在 ApplicationContext.xml
中配置时,就指定了,则在应用初始化时就把它 new 出来了,也就是说所有配置的 Bean
在程序一初始化时都给创建出来了,只等待你来用吧。这也就是有时为什么叫 Spring 的上下文为 Bean
容器的原因。(负面影响是,程序的初始化时间大延长了,但对于WEB应用而言,这并不是什么大不了的,谁你天天重启应用服务器呢?)


4. web.xml 文件中告诉我们,所有的Action 都是由这个 Servlet 处理的。(具体的内容会在后续的系列中介绍)

  <servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>


再次证实,所有的 Java Web 框架离不开 Servlet, 如果你们不再怀疑这句话,可以去买本 Servlet
的中文版的书,我是通过看所有的英文版的资料和API来学习的。虽然我没买过 Servlet
相关的书,但并不说明它不重要,恰恰是因为我当时也误解了。


搜索一下 china-pub, 发现所有与 Servlet 相关的书都是在2002 年之前出的,这说明, Servlet
是一个稳定的技术,自从规范 2.3 版(这一版加入了 Filter)以来,几乎没有引入新的特征,所以没有必要再写新书了,但据说下一版,
Java EE 6 将会有很大的变化,这是为了迎合新技术,如 AJAX。


实体关系图


实体关系图 (ERD - Entity Relationship Diagram, 以下是在 JDeveloper
中生成的,很方便,想知道怎么生成的话,我可以在 MSN
上告诉你们步骤,但你们先得告诉我你们想知道,这对于一个项目,如果是建立在已经存在的数据库之上时特别有用,因为一看到这图,所有的关系一目了然。
我想,随着你们的工作经历的增加,你们会意识到,一个项目如果是建立在没有存在的数据库之上时,这是罕见的,也是特别幸运的)



account 宠物店的用户表示

orders 购物清单

lineitem 购物清单中的中的每一项 (可以想象,我们到超市也可以一下买一条烟,两瓶酒
...)


item 包含商品的详细信息,比如 黄果树烟:单价
46元,厂家 贵州卷烟厂 ...

supplier 商品提供商,一般也称厂商

inventory 商品库存信息


实体关系映射(O/R Mapping)

从上面的实体关系图可以看到:

account (* .. 0..1) category, 这说明一个 Account 最多可以有一个自己最喜爱的宠物类型,也可以没有。

account (1 .. *) orders, 这说明一个 Account 可以包含多个 Order

Java代码中的关系表示

我们首先看看这两种关系在 Java 代码中的表示:


Account.java

public class Account implements java.io.Serializable ,Comparable {
private Long id;
private int version;
...

// fav <- favorite
private Category favCategory;
...

// @OneToMany(mappedBy = "user")
private Set<Order> orders =
new HashSet<Order>();



public Category getFavCategory() {
return favCategory;
}
public void setFavCategory(Category favCategory) {
this.favCategory = favCategory;
}


public Set getOrders() {
return orders;
}

public void setOrders(Set<Order> orders) {
this.orders = orders;
}

// scaffold code for collection field
public void addOrder(Order order) {
if (order == null)
throw new IllegalArgumentException("Can't add a null Order.");
this.getOrders().add(order);
}


Order.java

public class Order implements java.io.Serializable, Comparable {
private Long id;
private int version;

// @ManyToOne
// @JoinColumn(name="userId", nullable = true, updatable = false)

private Account user;

...


public Account getUser() {
return user;
}

Category.java
...

单向关系



在 Category.java 中没有看到我们希望看到的如下代码:

public class Category implements java.io.Serializable, Comparable {
private Long id; private int version;
// @OneToMany(mappedBy = "userId")
private Set<Account > users=
new HashSet<Account >();

...


这因为设计时考虑到从 Category -> Account
是没有必要的,如:当前显示一种宠物种类“狗”时,有必要显示出所有喜欢狗的用户出来吗?

既然没必要,所以我们没有将上面的代码加到Category 中去,这样叫单向关联(非双向关联,uni-bidirectional,
association),也就是说,在Java代码中,得到一个 Category 的实例后,是无法从它“导航”(navigate) 到
Account 的,但是反向则是可以的:


// 不可能
Category cate = ...;
Account a = cate.getUser();


// 只能

Account a = ...;

Category cate = a.getFavCategory();

双向关系



Account 与 Order 的关系是双向的,因为一个用户进行多次购物,每次都会生成一次清单。而每个购物清单都必须得到是谁的.



注:注释掉的 @OneToMany 与 @ManytoOne 等,是采用 Java 5 的 Annotation
进行的处理,如果所有的映射都进行这样的处理,我们可以排除使用 hibernate hbm.xml 文件。


从 Order 到 Account

Order o = ...

Account a = o.getUser();

但注意到 Order.java 中并没有 setUser()
方法,这时我们故意把 user 成员在 Order.java 做成了只读(read-only)了,为什么?


因为一个购物单生成了,是不能随便更改帐户的。如果要改,这个单子就作废。基于这个
考虑,只有这样设计才符合现实。



从 Account 到 Order

        HashSet<Order> orders = getOrders();
for (Iterator it = orders.iterator(); it.hasNext();) {
Object elem = (Object) it.next();
...
}





这里首先指出Account.java 中的几个 Bug:


1. 最好使用统一使用泛型 (gerneric type):

在声明时使用了,但在 getter, setter 中没有使用,前后不统一,因此应该为:

// @OneToMany(mappedBy = "user")
private Set<Order> orders = new HashSet<Order>();
...

public Set<Order> getOrders() {
return orders;
}

public void setOrders(Set<Order> orders) {
this.orders = orders;
}

// scaffold code for collection field
public void addOrder(Order order) {
if (order == null)
throw new IllegalArgumentException("Can't add a null Order.");
this.getOrders().add(order);
}

2. 理应将 setOrders(Set<Order> orders) 方法作为 private
的,或干脆去掉,因为有了脚手架方法(scaffold method) adOrder(Order order),Account
中所有对 Order 的处理应该由脚手架来处理,由它来操作这个集合类, Set<Order> orders,
并维护它们之间的这种双向关系,去掉了 setOrder
方法后,我们需要加另一个脚手架方法来实现从购物单去除一项的功能。(如果没有这项功能的话,那么在我们去超市排队付款时,当一个顾客想取消一个已扫描的
物品时,可能就会听到收银员说,“不好意思,不能取消!”,这显然是不合理的,所以我们需要增加 removeOrder 方法,如下:

    // @OneToMany(mappedBy = "user") 

private Set<Order> orders = new HashSet<Order>();

...


public Set<Order> getOrders() {
return orders;
}

//public void setOrders(Set<Order> orders) {
// this.orders = orders;
//}

// scaffold code for collection field
public void addOrder(Order order) {
if (order == null)
throw new IllegalArgumentException("Can't add a null Order.");
this.getOrders().add(order);
}

public void removeOrder(Order order) {
if (order == null)

throw new IllegalArgumentException("Can't remove a null Order.");


this.getOrders().remove(order);

}

Hibernate 的关系表示


好,看了我们熟悉的 JAVA 代码中处理这种关系后,我们得看看如果在 Hibernate 映射中声明这种关系了。

account -category

Account.hbm.xml

account (* .. 0..1) category (many-to-one)

        <many-to-one
name="favCategory"
column="favCategoryId"
class="Category"
foreign-key="FK_favCategoryId"/>


1. many-to-one 即是 *..1, 但是我们的关系其实是 *..0..1 即是 many-to-one 或 many-to-one
/ many-to-zero

但目前这种关系都只能通过 many-to-one 来指定,只不过,如果是严格的 many-to-one
关系时,我们要强调一个属性 non-null

        <many-to-one
name="favCategory"
column="favCategoryId"
class="Category"
foreign-key="FK_favCategoryId"
not-null=false />

这样就保证了非空.


2.外键 foreign-key, 如果不明确地写出,则 Hibernate
会自动产生一个随机的外键。象我们这样,我们可以在产生的数据库的定义语句中可以看到(如果使用的是 Mysql, 则在 Mysql Query
Browser 中,在hjpetstore 数据库中,选中 account | 右击 | copy SQL to
Clipbroad,然后粘贴在上面的输入框或其它任何文本编辑器的输入地方):

CREATE TABLE  `hjpetstore`.`account` (
`accountId` bigint(20) NOT NULL auto_increment,
`version` int(11) NOT NULL,
`username` varchar(80) NOT NULL,

...

`favCategoryId` bigint(20) default NULL,

...

PRIMARY KEY (`accountId`),
UNIQUE KEY `username` (`username`),
KEY `FK_favCategoryId` (`favCategoryId`),
CONSTRAINT `FK_favCategoryId` FOREIGN KEY (`favCategoryId`)
REFERENCES `category` (`categoryId`)

) ENGINE=InnoDB DEFAULT CHARSET=gbk;

3. 关系是由两端组成,指定时 xxx-to-xxx,
to 前面的关系端是当前映射文件对应的实体,在我们上面的例子即是 Account, to 后面的关系端是这个元素要表示的关系实体,

这个xml 元素(即 many-to-one) 的所有属性的指定都是用来约束 to 后面的关系端的,对于我们上面的例子是
Category.

  1. name="favCategory" 指的是 Account.java 中的成员

  2. column 指的是这个成员映射的的数据库列

  3. class 指的是 one 所代表的实体,即 Category

  4. foreign-key 即用来限制这个表列的值的外键,即是 Catggory 的主键


在自己手工写映射文件时,一定要清楚地知道这关系,或许你们现在只是被地接受,没有体会到自己写时的那种混乱:

  1. 在写 account.hbm.xml 时,为了表示这种关系是要指定 many-to-one 呢?还是 one-to-many 呢?

  2. one 指的是那个实体,many 又是那个实体,

  3. 在写 account.hbm.xml 是,为什么这里要指定的 class 是Category


  4. ...


为了弄清这些关系,

  1. 首先必须对 many-to-one, one-to-many,
    many-to-many(比较少见)有个清楚的认识,任何一种关系,把两端的实体对调,那么他们的关系就刚好要反过来,如:Account->
    category (many-to-one), category->account(one-to-many)


  2. 再有,在写 hbm.xml 文件时,记住,在指定关系映射时, to 前面的端是指当前这个 hbm.xml 文件代表的实体, to
    后面的端是对关系的另一端,而这个元素中的任何属性都用来标识并约束关系的另一端的。


Category.hbm.xml


category (0..1 .. *)
account (one-to-many)
, 刚好是account (* .. 0..1) category 的反向



同样,因为是单向关系,所以我们没有在其中看到如下的代码

        <!-- Mapping for Account association. -->
<set name="users"
inverse="true"
cascade="save-update"
access="field">
<key column="userId" />
<one-to-many class="Account"/>
</set>


从 account - > category 为 many-to-one, 那么反过来就是 one-to-many


account - order


生成的 SQL DDL:

CREATE TABLE  `hjpetstore`.`account` (

`accountId` bigint(20) NOT NULL auto_increment,
`version` int(11) NOT NULL,

...

`favCategoryId` bigint(20) default NULL,

...

PRIMARY KEY (`accountId`),
UNIQUE KEY `username` (`username`),
KEY `FK_favCategoryId` (`favCategoryId`),
CONSTRAINT `FK_favCategoryId` FOREIGN KEY (`favCategoryId`)
REFERENCES `category` (`categoryId`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;



CREATE TABLE `hjpetstore`.`orders` (

`orderId` bigint(20) NOT NULL auto_increment,
`version` int(11) NOT NULL,
`userId` bigint(20) NOT NULL,

...

PRIMARY KEY (`orderId`),
KEY `FK8D444F05BC594D77` (`userId`),
CONSTRAINT `FK8D444F05BC594D77` FOREIGN KEY (`userId`)
REFERENCES `account` (`accountId`)

) ENGINE=InnoDB DEFAULT CHARSET=gbk;


Account.hbm.xml


account (1 .. *) order (one-to-many)

        <!-- Mapping for Order association. -->
<set name="orders"
inverse="true"
cascade="save-update"
access="field">
<key column="userId" />
<one-to-many class="Order"/>
</set>


1. set 即是 代表集合类 Set

2. name="orders" 即是 Account.java 的成员为 orders

3. access="field" 即通过类的成员来访问 orders, 不管其是否为 public 与否;而不是通过
getter/setter 来访问,如果是后者的话,则为 access="property",但此时必须定义 getter 和 setter,
但我们不是把 setOrders() 注释掉了吗?所以只能是 field 了.

4. <key column="userId" /> 这个是用来定义这种外键关系的名字, 所以我们可以在表 Order
产生的 DDL 中可以看到 foreign key('userId'),Order的这个外键引用的正是Accoung的主键, accountId: references 'accoung' ('accountId'); 但请注意,我们是在 Account.xml 定义的这些关系,但是在关系的 many 端,我们的例子中(Order),生成的表中才会出现这种约束关系,这与我们写 SQL 创建表是一样的:

外键约束关系只会出现在关系的 many端,因为只有关系的多端才需要这个外键来指定这个关系的单端是谁。Order1 需要知道这是order 是属于谁,Order2 也是需要知道这个
order 是属于谁;但是 Account 要么知道他所有的 order, 要么知道他根本还没有 order, 并不可能用
account 来限定表 order 中的一行;所以这就是为什么外键约束总是出现在 many 端的原因吧。


但在 java 代码中这个外键关系刚刚相反,它总是以 集合类的方式出现在单(one) 端中,比如 orders 存在于 Account.java,所以这种映射也是出现在单端的 hbm.xml 文件中,而在多端中往往只是普通的一个实体引用:

  // @ManyToOne
// @JoinColumn(name="userId", nullable = true, updatable = false)
private Account user;

所以一定注意。


5. <one-to-many class="Order" /> 这个应该是比较明显了,声明多端,也就是集合类中元素的类型

6. inverse="true"

7. cascade="save-update"


上面两个属性,是值得好好理解的,因为这个两个属性,对于熟悉SQL及ERD的人来说也并不是容易理解的,因为这个两个属性或多或少是必于
Hibernate或所有 O/R Mapping 框架特有的东西。如果需要深入了解的话,看看《Hibernate In Action》
3.7.4 Making the association bidirectional 是有好处的。不过我这里也想好好讲讲,毕竟这个太重要了。


为了更好地理解这两概念,现在我们先假设 Order 的成员 user 不再是只读,即存在 setUser 方法,如下:

  public void setUser(Account account) {
this.user = account;
}


因为我们这个例子是个特例,read-write 的成员还是占大多数的。


现在,如果在客户端有如下代码:

Order order = ...;
Account account = ...;

order.setUser(account); account.addOrder(order);


引用自 Hibernate In Action:

This code is fine, but in this
situation, Hibernate detects two different changes to

the in-memory persistent instances.
From the point of view of the database, just


one value must be updated to reflect
these changes: the UserId(原文为ITEM_ID) column of the

Order(原文为BID) table. Hibernate doesn’t
transparently detect the fact that the two changes refer to the

same database column, since at this
point we’ve done nothing to indicate that this is a bidirectional

association.
We need one more thing in our
association mapping to tell Hibernate to treat

this as a bidirectional association:
The inverse attribute tells Hibernate that the

collection is a mirror image of the
many-to-one association on the other side:

...


Without the inverse attribute, Hibernate would try to execute two
different SQL

statements, both updating the same foreign key column, when we
manipulate the

association between the two instances. By specifying inverse="true", we
explicitly

tell Hibernate which end of the association it should synchronize with
the database.

In this example, we tell Hibernate that it should propagate changes made

at the Order
(原文为Bid) end of the association to the database,
ignoring changes made only to


the orders(原文为bids) collection. Thus if we only call
account.addOrder(order) (原文为 item.getBids().add(bid)), no changes

will be made persistent. This is consistent with the behavior in Java
without

Hibernate: If an association is bidirectional, you have to create the
link on two

sides, not just one.



这两行代码可以正常编译,但是在这种情形下,Hibernate 将发现这里存在两个改变试图改变内存中的持久化对象(为什么叫内存中,而不是数据库中呢?因为Hibernate
会把所有它碰到过的实体对象缓存在一级 Cache ,即所谓的 session
cache中,注意,稍后我们在配置文件中配置的是二级cache,它是在 SessionFactory 级的
)。但从数据库方面来看,
这只需要一个改变即可以反映这种改变(这种改变指的是建立起这种关系,将这两个新建的实体关联起来,即指定 Order 表中的 UserId
列,即外键)。Hibernate 并不明确地知道这一事实:这这两个改变都是指向同一个数据库表的列,因为在此时我们还没有指示这种关系是一个双向关系


我们需要在关联关系映射中进一步告诉 Hinbernate 这两个实体的这种关系对待为双向关系:inverse 属性告诉
Hibernate 这个集合(即单端中的集合成员,在我们的例子中即为 Account 中的 orders)是另一端的 many-to-one
关系的镜像。(请接下一段翻译)(另一端的 many-to-one 关系即是下面的映射


order (* .. 1) account (many-to-one)


    <many-to-one name="user"
column="userId"
not-null="true"
update="false"
access="field" />


我们不重复上面已经介绍过或一看就能理解的属性,如 many-to-one, access 等。

1. not-null="true" ,即会在SQL中产生:

`userId` bigint(20) NOT NULL,

2. update="false" 即是这个属性是 read-only
的 (我们又回到了将 setUser() 方法去掉的情形,因为我们上面的假设只是为了更好地描述问题,因此只要是双向关系就必须在集合端指示地指定 inverse="true'))



(上接)

...

没有 inverse 属性,在我们操纵两个实体间的关系时,Hibernate 将会试图执行两个不同的SQL语句来更新同一个外键列。通过指定
inverse="true", 我们明确地告诉 Hibernate 关系的哪端应该将改变同步到数据库中去(即将改变通过 SQL
语句写入到数据库中去),在这个例子中,我告诉 Hibernate 应该将发生在 Order
端的改变传播到数据库中去,而忽略仅仅发生在单端中的集合类成员 orders 的这端改变(即单端中的集合类 )。因此,如果我们仅仅通过调用
account.addOrder(order),
将不会有任何作用(即不会将这种改变存到数据库中去,也就是说,这种改变在程序退出后将消失。很危险是吗,别着急,请看下一段!)。这种行为是和普通 java
的行为一致的,如果关系是一个双向关系时,我们不得不在关系的两端创建出一个链接,而不是一端。


正如上段所说,为了达到不因为更改同一个数据库列而执行两条SQL语句,我们付出了
很大代价,改变发生在单端集合成员上的改变如果不明确地调用 session.save(...); 的话,将有可能丢失数据



难道 Hibernate 号称它为透明化持久(transitive persistence) 是徒有虚名的?
不!这正是 cascade 的作用。



我们上现在的例子中通过指定:

cascade="save-update"

详情请见 Hibernate In Action:

4.3.2 Cascading persistence with Hibernate


You can map entity associations in metadata with the following
attributes:

  • cascade="none", the
    default, tells Hibernate to ignore the association.


  • cascade="save-update"
    tells Hibernate to navigate the association when the transaction is
    committed and when an object is passed to save() or update() and save
    newly instantiated transient instances and persist changes to detached
    instances.

  • cascade="delete" tells
    Hibernate to navigate the association and delete persistentinstances
    when an object is passed to delete().

  • cascade="all" means to
    cascade both save-update and delete, as well ascalls to evict and lock.

  • cascade="all-delete-orphan"

    means the same as cascade="all" but, in addition,Hibernate deletes any
    persistent entity instance that has been removed(dereferenced) from the
    association (for example, from a collection).

  • cascade="delete-orphan"
    Hibernate will delete any persistent entityinstance that has been
    removed (dereferenced) from the association (forexample, from a
    collection).


cascade="save-update" 告诉
Hibernate,当 session.save() 或 session.update()
后,事务操作被提交(committed)之后中,它将导航(natigate)到关系的另一端,将保存新创建的实体实例和对
detached(这个词的翻译需要一段话来解释,简单地讲,是因为 Hibernate
会一次性把好多对象从数据库中取出,然后断开数据库连接,这时,这些取出的对象就处在 detached
状态,而后,对这些对象的改变会在适当的时候通过重新获得数据库连接保存到数据库当中去)作出的改变。



至于其它的用到的应该不多特别是 all, all-delete-orphan, delete-orphan, 为什么叫 orphan,
举例来说,如果一个用户销户了,从Account表中删除了,而数据库Order表中还保存了他的购物清单,这时这些 order
数据就叫做孤儿,因为此时从 Account 永远访问不到 Order 了。

但是现实当时,除非特别指出,一般还是让这些孤儿保留比较好,省得一下子所以的记录全没了。所以一般设置为 sava-upate 就可以了。



注意:

我映射 POJO类 Order 时走过弯路,因为 order 是SQL语言的关键字,所以总是出错,最终我看错误信息是有关 (key word) ,所以我把表的名字加上了一个 's',如:


  <!-- Order 为数据库关键字,所以表名不能为 Order.
这可是花了几小时的代价得出的经验
-->
<class name="Order" table="Orders" lazy="true">


主键映射


Account.hbm.xml 中



        <id name="id" type="long" column="accountId" 
unsaved-value="null"
access="field">
<generator class="native" />
</id>

<!-- A versioned entity. -->
<version name="version"
access="org.hibernate.property.DirectPropertyAccessor" />


Order.hbm.xml 中

  <id name="id" type="long" column="orderId" unsaved-value="null"
access="field">
<generator class="native" />
</id>

<version name="version" access="org.hibernate.property.DirectPropertyAccessor" />


1. <id> 是专用元素,用来指定主键


2. <version> 是Hibernate用来作优化处理的, 如果熟悉Hibernate 的 API 的话,会知道它有
session.save(), session.update(), session().saveOrUpdate();而方法
saveOrUpdate() 是方便调用端不用管这个对象是新建的还是前头已创建后这次只进行了更新;这个决定 Hibernate
应该比我们更清楚,因此交给它去做吧。但这个更清楚是需有个前提的,这个前提即是 <version> 它联合 <id 元素的
unsaved-value="null" 属性一起用来区分一个实体对象是新创建的还是已经持久化过后进行更新的。


3. <generator> 元素是指定这个主键产生的机制:


Hibernate 3.x manual 中 5.1.4.1
Generator,(因为 Hibernate in action
比较老,它没有明确地列出后续版本支持的机制,所以我们这里引用这个文档)有所有值的描述


我们之所以指定为 native,是因为我们这个演示项目建立在没有现在数据库的基础上,所以我们完全可以选择最好的机制产生主键。native
会使用你指定的数据库方言来决定使用最适合这种数据库的机制。如:

oracle 将会使用我们熟悉的 Sequence(Hibernate 会自动生成一个 hibernate_sequence,而所有被指定为
native 的主键将从这个唯一的序列发生器中取出下一值来作为 id 的主键值),

其它几种常用的数据库 Mysql,DB2 和 MS SQL Server 将会使用 identity


但是正如我前面提到的,项目建立在没有遗留数据的情况是罕见的!大多数项目不得不面对一大堆没有很好建立起约束关系,而且使用“自然主键”
(natural primary key)作为主键,这与 Hibernate 的设计理念背道而驰。在Hibernate
看来,主键只是用来唯一地标识这条数据行的,除些之外,不能再有其它含义,但现实中,往往出现
SID(保险号,身份证号)等具有相应含义的列来作为数据库的主键,关于自然主键请参阅:

3.4.3 Choosing primary keys

8.3.1 Legacy schemas and
composite keys


我们说了,如果指定为 native 的机制的话,在初始化一个实体对象时,你根本不需要管这个 id 属性,也就是说你看到带有参数 id
的构造函数,也不可能有setId(long id) 方法,因为hibernate 会生成且只生成一个序列发生器(对 oracle)
,且用其中的值来应对所有的数据表,如:


create sequence hibernate_sequence start with 1;


初始化了第一个 Order, 则 orderId = 1;

初始化了第二个 Order, 则 orderId = 2;

初始化了第一个 Account, 则 accountId = 3;

初始化了第二个 Account, 则 accountId = 4;

初始化了第三个 Order, 则 orderId = 5;


这里存在几个问题首先每个表的主键不再是连续的,再值或多或少依赖于 start with 的值。


在现实中,自然主键往往一般是连续的,且其长度都是受限的,比如身份证为 15 或 18 位。所以,对于已存在的数据库,只能使用机制 assign:

assigned

lets the application to assign an identifier to the object
before save() is called. This is the default
strategy if no <generator> element is
specified.

这就是说,数据库主键是通过构造函数传进来的,然后这个实体通过调用 session.save(new
abcEntity(abcId, ...)); 进行持久化的。此时, Hibernate
将责任交给了你,程序员来做这件事,而且你也正想做这件事。不是吗?如果不这样你怎么来保证你的主键与数据期待的条件相符?。

或者,如果你的实体对象只提供了默认构造函数,那么此时你必须提供一个 setId(...) 的方法,来给实体对象设置主键。

其他映射


至于其它的与 java
代码中的成员一一对应的映射,我想理解了上面的内容的话,应该不会有太多困难。所以请你们打开IDE一一对应着看,如果有不明白的就及时问问,我将乐意解答。

  • 组件关系,如Account中对 userAddress 的映射:<component name="userAddr"
    ...>

  • 实体生命周期的管理:父子关系,这也是UML中的强互合关系(如:人与胳膊的关系,人是需要负责胳膊的生命周期管理的,即人不存在了,胳膊也
    就不复存在了。)

  • 映射类的继承关系

  • 多对多关系

二级缓存映射


这一主题是我目前还未深入的,详情请见 Hibernate 3.xmanual 19.2. The Second Level Cache



最主要的是在映射中加上

<cache usage="read-write"/>


<cache usage="read-only/>


或其它更高级的设置。


read-only, 就是说实体对象被从数据库中取出后,不再会发生改变时,指定这个属性可以获得更好的性能。

但如果需要改变时,你别无选择,必须指定 read-write.


象这个项目中的与库存有关关的实体就是 read-only的,因为我们这个例子并没有包括后的库存管理功能,如果包含了的话,则Category/Inverntory/Product 等等可能也不能指定为read-only 了,因为它们也需要在管理后台中进行更新的。

Hibernate 配置文件

Hibernate 的全局配置一般会在 Hibernate.cfg.xml 文件中,但我们这个项目使用了
Spring, 所以它以Spring 的方式集成进来了,这就是 WEB_INF 下面的几个 dataAccessContext-
开头的几个文件的内容,具体用哪个会在部署系列中详细介绍的。目前是用哪个,看 web.xml 中的定义:

  <!--
- Location of the XML file that defines the root application context.
- Applied by ContextLoaderServlet.
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- local datasource -->
<param-value>
/WEB-INF/dataAccessContext-hibernate.xml /WEB-INF/applicationContext.xml
</param-value>


<!-- jndi datasource and JTA (for a transactional JNDI DataSource)
<param-value>
/WEB-INF/dataAccessContext-hibernate-jndi.xml /WEB-INF/applicationContext.xml
</param-value>
-->
</context-param>


正如我单独列出一小结,我本想详细介绍一下这个文件的内容,但看了一下,如果仔细读了系列一的话,其实大部分内容已经提及过了,且 dataAccessContext-hibernate.xml文件的内容还算轻松。你们好好看看,看有问题否?


其实这个文件中只有以下几项内容,且其内容是一项项嵌套的:

  1. dataSource 数据源的指定,除非你们公司有更好的实现,一般可能就会是 C3P0 了。它被 sessionFactory使用了

  2. sessionFactory, 关键是它包含的
    <property>子元素,一一指定吧。其中指定了二级缓存的的提供者:hibernate.cache.provider_class。
    它被 transactionManager 和所有DAO使用了

  3. transactionManager 事务管理器,管理所有从 <property> 中指定的
    sessionFactory 获得的 session 操作的事务


  4. 剩下就是所有 DAO 的配置了

Ehcache


象我们的第二项中指定了二级缓存的提供者为:

        <prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider</prop>

那么默认情况下 EhCacheProvider 将会在 CLASSPATH 中寻找它的配置文件 ehcache.xml,
一般情况下,它会放在源文件的包的根目录下,这就是在 netbeans 显示在Projects 中 Source Packages 下的
<default package> 中的 ehcache.xml 文件。但如果 Ehcahe
没有在类路径中找到配置文件,那么它会启动它的故障防护功能,会使用默认的配置ehcache-failsafe.xml,展开库(Projects
中 hibernateJpetstore | libraries | encache-1.2.3.jar | 无名包 |
ehcache-failsafe.xml).


这个配置文件中的属性有:

<ehcache>

<!-- Sets the path to the directory where cache .data files are created.

If the path is a Java System Property it is replaced by
its value in the running VM.

The following properties are translated:
user.home - User's home directory
user.dir - User's current working directory
java.io.tmpdir - Default temp file path -->
<!--
指定缓存文件存放的目录
在运行的服务器的 tmp 目录下,如:
.netbeans\5.5dev\apache-tomcat-5.5.17_base\temp\ehcache
-->

<diskStore path="java.io.tmpdir/ehcache"/>


<!--Default Cache configuration. These will applied to
caches programmatically created through
the CacheManager.

The following attributes are required: (必须的选项)
- Sets the maximum number of objects that will be created in memory
- (最多多少个对象将被缓存在内存中)

maxElementsInMemory

- Sets whether elements are eternal.
- If eternal, timeouts are ignored and the
eternal

- Sets whether elements can overflow to disk when the in-memory cache
- has reached the maxInMemory limit.
- (是否将溢流的元素,即超过第一项配置的最大个数时,是否将元素写到硬盘中去?)

overflowToDisk


The following attributes are optional: (可选的选项)

- Sets the time to idle for an element before it expires.
- i.e. The maximum amount of time between accesses before an element expires
- Is only used if the element is not eternal.
- Optional attribute. A value of 0 means that an Element can idle for infinity.
- The default value is 0.
    - (元素可以呆在内存中的最长的空闲时间,即两次访问之间的间隔,如果超过这个数,元素将过期,即从缓存中移走,
- 下次再访问这个元素时,需从持久化存储中取出。仅对非外部化元素有效。默认为0,表示元素永不过期)
timeToIdleSeconds

- Sets the time to live for an element before it expires.
- i.e. The maximum time between creation time and when an element expires.
- Is only used if the element is not eternal.
- Optional attribute. A value of 0 means that and Element can live for infinity.
- The default value is 0.
- (元素在创建后多少秒后将被销毁, 默认为0,表示永远不销毁)

timeToLiveSeconds

- Whether the disk store persists between restarts of the Virtual Machine.
- The default value is false.
-(在JVM重启时,是否将内存中的元素写到硬盘。默认为 false)

diskPersistent

- The number of seconds between runs of the disk expiry thread. The default value
- is 120 seconds. (设定缓存在硬盘上的生存时间)
diskExpiryThreadIntervalSeconds
-->

<defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
</ehcache>


上面这个文件指定的是默认配置,但如果要对某一实体进行特定的配置时,只需要追加一个元素,取名为该实体的名称即可。

<cache name="org.springframework.samples.jpetstore.domain.Order"


maxElementsInMemory="10000"

eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"

overflowToDisk="true" />


总结



是的,要短时间内完全理解 O/R mapping
是不容易的。但理解那怕一部分都有可能对今后的项目的ER(实体关系)有更清楚的认识,并设计出更加合理的数据层。

其实,这些不光对 Hibernate 有用,对 EJB 实体 Bean, 对普通的JDBC的数据库建模也同样有效的。



理解了最困难的部分,后续的系列将会简单多了。下一系列,控制层。

没有评论: