您的位置:首页 > 编程语言 > Java开发

Hibernate + Struts 学习笔记 (作者: Annhy )

2004-09-10 10:11 495 查看
写在前面

最近开始学 Hibernate + Struts,大概是因为我资质驽钝,总觉得看的东西不算少(这要感谢各位前辈),但是写起程序来就是不太顺。所以我想把一些心得与疑问贴在这里,一则可以向各位请教,再则也可以提供后来学习的人参考。各位如果觉得我写的有问题,还请一定要提出批评与指教,有批评我才会进步,有指教我会进步得更快。我会先从后端的 Hibernate 开始进行,然后再加上前端 Struts 的部分。既然是沿路将相关的工作内容与疑问记下,可能会看起来有点混乱,还请忍耐。希望我有恒心与毅力把它完成...
为了避免侵犯我们公司的智慧财产权,以及营业秘密等头痛的问题,我决定不使用目前公司所上线使用的系统当例子 (而且它的商业逻辑已经变得有点庞大,大到我不知道怎么拿来当作教学文件了),改用比较像是小玩具等级的程序当作范例程序,之前的那个例子就让它随风而逝吧...

我参考的数据:
1. hibernate 快速入门 http://www.javaworld.com.tw/jute/post/view?bid=11&id=3291&sty=1&tpg=1&age=-1 2. Introduction to Hibernate from theserverside http://www.theserverside.com/resources/articles/Hibernate/IntroductionToHibernate.pdf 3. Hibernate2 Reference Documentation http://www.hibernate.org/hib_docs/reference/pdf/hibernate_reference.pdf ================================================================================
功能需求

我们要开发一个(非常阳春的)相片管理系统,它的功能非常的简单。大致如下:
1. 提供对相片的 新增/修改/删除/查询 的功能。
2. 为了能够比较方便的管理相片,我们要建立相片的分类 (也就是相簿),此分类为阶层式的树状结构,也就是说 leaf node 为相簿,non-leaf node 为相簿之分类。(分类也要有 新增/修改/删除/查询 的功能)
3. 一个相簿中可以有多张相片,一张相片也可以同时归类于不同的相簿中。

类别设计

根据刚刚所定义的需求,可以很快的定出两个 domain class,那就是 Photo 与 Album。

1. Photo: 纪录相片的相关信息
Photo {
id : Long; // persistent 时所用的唯一识别码
fileName : String; // 檔名
title : String; // 照片标题
description : String; // 照片说明
albums : Set; // 此照片所属的相簿
}

Thinking 1:
id 的数据型态用 Long,而不是 long,我看到的 Hibernate 范例几乎都是这样用的。这是因为 Long 可以有 null 值,用来判断尚未被 Hibernate 存入数据库的对象很方便。若用 long,0 或 -1 就可能比较容易会不小心与真正的值相冲突。
另外,为什么不用 Integer 呢?这我不知道,大概是怕 integer 的范围不够大,如果设计时没考虑到,等到上线时爆掉,就很麻烦吧...

Thinking 2:
因为 albums 中不应该出现重复的分类,所以用 Set 而不用其它的 Collection,可以避免发生不小心的情况。另外 albums 的排序,我想应该用分类的 id 来当作排序依据。

2. Album: 纪录 Photo 的分类阶层架构

Album {
id : Long; // persistent 时所用的唯一识别码。
title : String; // 相簿标题
description : String; // 相簿说明
photos : Set; // 属于此相簿之所有相片对象
parent : Album; // 上层相簿分类
children : Set; // 属于此分类之下层相簿对象
}

Thinking 1:
与 Photo.albums 相同的理由,所以用 Set。这是个双向的 association,想不起来哪里看过一个条款,它说如果双向连结不好维护或效率很差,就把它改成单向的。不过既然 Hibernate 的范例是这样写,那我就先这样用了,应该没问题吧...

Thinking 2:
因为整个相簿分类是一个树状结构,所以需要有 parent 与 children 来记录相互间的架构关系。
这里又扯出一个问题,那就是树状结构是否应该有一个共同的根?也就是说如果我有三大分类 [家人], [学生时代],[其它],我们是否应该为它们建立一个共同的父节点呢?
我个人是比较倾向建立这样的 (虚拟) 共同父节点 (就取为 "我的相簿" 好了),这样写程序时,可以减少许多判断上的工作量。

我的相簿
|
+--家人
| |
| +--亲爱的老婆大人
| |
| +--宝贝女儿
| 
+--学生时代
| |
| +--大学
| |
| +--研究所
| 
+--其它

================================================================================

类别实作

因为 Album 与 Photo 是 domain objects,所以放在 annhy.photo.domains 这个 package 之下。
根据之前的类别设计,我们可以很容易地把它们实作出来,但它们目前只有 private data member 与 getter/setter methods,是不折不扣的 Java Bean。

这样还不够,为了要使它们更好用,我们还要加上一些东西:
a. 提供额外的建构式。除了预设建构式以外,再加上传入一个字符串参数 (fileName) 的建构式。
b. 覆写 Object 类别的 toString(), equals(), hashcode() 等函式。
c. 实作 Comparable 接口,以及 compareTo() 函式。
d. 套用 Encapsulate Collection (封装群集) 的重构手法。
(注: 在 Refactoring 一书的 p.208 有一个 Encapsulate Collection (封装群集) 的手法。按照它的说法,我不应该为整个群集 (例如: Photo.albums) 提供 getter/setter,而应该提供用以为群集 add/remove 元素的函式。所以我为这两个 class 加上相对应的函式,但暂不将原有 getter/setter 移除,以免 Hibernate 发生危险。)
e. 提供 finder methods,以便搜寻对象。因为 finder methods 与 Hibernate 的藕合度较高,所以把它们抽离出来,移至 Albums.java 与 Photos.java。(晚点再写)

Photo.java:

package annhy.photo.domains;
import java.util.*;
/**
* 用来表示 Photo 数据的对象。
*
* @author Annhy
* @version 1.0, 2003/10/01
*/
public class Photo implements Comparable {
//================
//== Constructors
//================
public Photo() {}

// 为了方便起见,多加一个建构式
public Photo(String fileName) {
this.fileName = fileName;
}

//================
//== data members
//================
private Long id;
private String fileName;
private String title;
private String description;
private Set albums = new TreeSet(); // 有顺序,所以用 TreeSet

//================================
//== getter & setter methods
//================================
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}

public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}

public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}

public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}

public Set getAlbums() {
return albums;
}
public void setAlbums(Set albums) {
this.albums = albums;
}

//====================
//== override methods
//====================
/**
* 覆写 Object.toString()
* @return debug 用的字符串
*/
public String toString() {
return "[Photo(" + id + "): " + fileName+ "]";
}

/**
* 覆写 Object.equals()
* @param other 欲比较的另一个对象
* @return 两对象是否相等
*/
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Photo)) {
return false;
}
Photo that = (Photo) other;

return this.id.equals(that.id);
}

/**
* 覆写 Object.hashCode(),这样才能用于 hash 对象
* @return 物件的 hash code
*/
public int hashCode() {
return this.id.hashCode();
}

/**
* 实作 Comparable.compareTo()
* @param other 欲比较的另一个对象
* @return 比较大小的结果
*/
public int compareTo(Object other) {
Photo that = (Photo) other;
return this.id.compareTo(that.id);
}

//====================================
//== Encapsulate Collection (封装群集)
//====================================
public void addAlbum(Album album) {
albums.add(album);

// 加入反向连结 (要先判断!!)
if (!album.getPhotos().contains(this)) {
album.addPhoto(this);
}
}

public void removeAlbum(Album album) {
albums.remove(album);

// 移除反向连结 (要先判断!!)
if (album.getPhotos().contains(this)) {
album.removePhoto(this);
}
}

public void clearAlbums() {
// 记录原来的 snapshot
Album[] arrSnapshot = (Album[]) albums.toArray(new Album[0]);
for (int i = 0; i < arrSnapshot.length; i++) {
removeAlbum(arrSnapshot[i]);
}
}
}

Album.java:

package annhy.photo.domains;

import java.util.*;

/**
* 用来表示相片分类 (相簿) 的对象。
*
* @author Annhy
* @version 1.0, 2003/10/01
*/
public class Album implements Comparable {
//================
//== Constructors
//================
public Album() {
}

// 为了方便起见,多加一个建构式
public Album(String title) {
this.title = title;
}

//================
//== data members
//================
private Long id;
private String title;
private String description;
private Set photos = new TreeSet(); // 有顺序,所以用 TreeSet
private Album parent;
private Set children = new TreeSet(); // 有顺序,所以用 TreeSet

//================================
//== getter & setter methods
//================================
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}

public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}

public Set getPhotos() {
return photos;
}
public void setPhotos(Set photos) {
this.photos = photos;
}

public Album getParent() {
return parent;
}
public void setParent(Album parent) {
this.parent = parent;
}

public Set getChildren() {
return children;
}
public void setChildren(Set children) {
this.children = children;
}

//====================
//== override methods
//====================
/**
* 覆写 Object.toString()
* @return debug 用的字符串
*/
public String toString() {
return "[Album(" + id + "): " + title + "]";
}

/**
* 覆写 Object.equals()
* @param other 欲比较的另一个对象
* @return 两对象是否相等
*/
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Album)) {
return false;
}
Album that = (Album) other;

return this.id.equals(that.id);
}

/**
* 覆写 Object.hashCode(),这样才能用于 hash 对象
* @return 物件的 hash code
*/
public int hashCode() {
return this.id.hashCode();
}

/**
* 实作 Comparable.compareTo()
* @param other 欲比较的另一个对象
* @return 比较大小的结果
*/
public int compareTo(Object other) {
Album that = (Album) other;
return this.id.compareTo(that.id);
}

//====================================
//== Encapsulate Collection (封装群集)
//====================================
public void addPhoto(Photo photo) {
photos.add(photo);

// 加入反向连结 (要先判断!!)
if (!photo.getAlbums().contains(this)) {
photo.addAlbum(this);
}
}

public void removePhoto(Photo photo) {
photos.remove(photo);

// 移除反向连结 (要先判断!!)
if (photo.getAlbums().contains(this)) {
photo.removeAlbum(this);
}
}

public void clearPhotos() {
// 记录原来的 snapshot
Photo[] arrSnapshot = (Photo[]) photos.toArray(new Photo[0]);
for (int i = 0; i < arrSnapshot.length; i++) {
removePhoto(arrSnapshot[i]);
}
}

public void addChild(Album child) {
children.add(child);

// 加入反向连结 (要先判断!!)
if (!this.equals(child.getParent())) {
child.setParent(this);
}
}

public void removeChild(Album child) {
children.remove(child);

// 移除反向连结 (要先判断!!)
if (this.equals(child.getParent())) {
child.setParent(null);
}
}

public void clearChildren() {
// 记录原来的 snapshot
Album[] arrSnapshot = (Album[]) children.toArray(new Album[0]);
for (int i = 0; i < arrSnapshot.length; i++) {
removeChild(arrSnapshot[i]);
}
}
}

目前档案列表:
/annhy/photo/domains/Photo.java
/annhy/photo/domains/Album.java

================================================================================

设定 Hibernate 相关档案

class 初步实作好之后,就可以设定 Hibernate 相关的档案了。
这里有三个档案要做: hibernate.cfg.xml, Photo.hbm.xml, Album.hbm.xml
(本来是使用 hibernate.properties,改用 hibernate.cfg.xml 可以有额外的功能,
感谢 browser 的指导 )

hibernate.cfg.xml:

<?xml version="1.0" encoding="Big5"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- session factory properties -->
<property name="dialect">net.sf.hibernate.dialect.MySQLDialect</property>
<property name="connection.driver_class">org.gjt.mm.mysql.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/photo_db?useUnicode=true&characterEncoding=Big5</property>
<property name="connection.username">photo_db</property>
<property name="connection.password">photo_db</property>

<!-- domain object 的对应档案 -->
<mapping resource="annhy/photo/domains/Photo.hbm.xml"/>
<mapping resource="annhy/photo/domains/Album.hbm.xml"/>
</session-factory>
</hibernate-configuration>

这里要注意的是这个特殊的字符串: ?useUnicode=true&characterEncoding=Big5 ,听说这是因为 MySQL 不支持 UniCode,如果不加上这些,存入中文数据就会有问题。还有,因为这是 XML 格式的档案,所以 & 要替换为 &amp;(请自己自己换成半角!! 为了这个问题,浪费了我几个小时...)

Photo.hbm.xml:

<?xml version="1.0" encoding="Big5"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" >
<hibernate-mapping>
<class name="annhy.photo.domains.Photo" table="photo">
<id name="id" column="photo_id">
<generator class="native" />
</id>
<property name="fileName">
<column name="fileName" sql-type="text" />
</property>
<property name="title">
<column name="title" sql-type="text" />
</property>
<property name="description">
<column name="description" sql-type="text" />
</property>
<!-- Photo 与 Album 的 n:n 对应关系 -->
<set name="albums" table="rel_album_photo" lazy="false" sort="natural">
<key column="photo_id" />
<many-to-many class="annhy.photo.domains.Album" column="album_id" />
</set>
</class>
</hibernate-mapping>

Album.hbm.xml:

<?xml version="1.0" encoding="Big5"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" >
<hibernate-mapping>
<class name="annhy.photo.domains.Album" table="album">
<id name="id" column="album_id">
<generator class="native" />
</id>
<property name="title">
<column name="title" sql-type="text" />
</property>
<property name="description">
<column name="description" sql-type="text" />
</property>
<!-- Photo 与 Album 的 n:n 对应关系 -->
<set name="photos" table="rel_album_photo" inverse="true" lazy="false" sort="natural">
<key column="album_id" />
<many-to-many class="annhy.photo.domains.Photo" column="photo_id" />
</set>
<!-- Album 自己的树状阶层架构 -->
<!-- 指向 parent -->
<many-to-one name="parent" column="parent_id" class="annhy.photo.domains.Album" />
<!-- 指向 children -->
<set name="children" inverse="true" lazy="false" sort="natural">
<key column="parent_id" />
<one-to-many class="annhy.photo.domains.Album" />
</set>
</class>
</hibernate-mapping>

这里要注意的是:
1. 当使用了双向连结,其中一个 class 必须要设定 inverse="true"才行。至于是设定哪一边的 class,似乎没啥影响。
2. 因为在 MySQL 中 java.lang.String 预设对应为 varchar(255),这个 size 通常不够用,所以要用下面这种写法,强制它对应到 text。( 感谢 chrischang 的指导

<property name="title">
<column name="title" sql-type="text" />
</property>

这个 Album.hbm.xml 展示了 many-to-many 的关系,以及树状阶层架构。这两种特殊用法,之前我都找不到范例,只好自己 try。这就是我 try 出来的结果,还不错,很直觉。

Thinking 1:
在设定对应关系的地方,我都有设定 lazy="false" (不使用 Lazy Initialization),而且不能设定 class 的 proxy 属性。这是因为之前我设定过这两个选项时,如果在关闭 session 之后,才去读取 photo.albums或 albums.children 这些 collection 就会产生 error。
我知道 Hibernate 就是这样设计的,但我不懂,难道 session 用完可以不关闭吗?(手册上面这么描述 Session: A single-threaded, short-lived object ....) 如果是 Web AP,每一个 HTTP request 都算是独立的动作,这样难道不会有问题? 在我还搞不懂之前,我还是先不要乱用好了,反正在数据量不多的情况下,顶多只是效率比较差罢了...

有没有善心人士可以指点我一下...

目前档案列表:
/annhy/photo/domains/Photo.java
/annhy/photo/domains/Album.java
/hibernate.cfg.xml
/annhy/photo/domains/Photo.hbm.xml
/annhy/photo/domains/Album.hbm.xml

================================================================================

ThreadLocalSession.java, Photos.java, Albums.java

到这里,已经写好 domain class 与 Hibernate 相关档案,我们已经有足够的东西
来产生测试数据库。

我要先提供一个用以取得 session 的公用类别,因为我有用到 web 的程序,
所以此公用类别必须是 thread-safe 的,于是我参考了 http://hibernate.bluemars.net/42.html http://hibernate.bluemars.net/114.html
之后,使用 ThreadLocal 变量来解决。

ThreadLocalSession.java:

package annhy.photo.hibernate;

import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;

public class ThreadLocalSession {
// Hibernate 的设定环境对象,由 xml mapping 文件产生
private static Configuration config;
// SessionFactory
private static SessionFactory factory;
// ThreadLocal 变量,用来存放 ThreadLocalSession 对象
private static final ThreadLocal sessionContext = new ThreadLocal();

static {
init();
}

private static final void init() {
try {
config = new Configuration().configure();
factory = config.buildSessionFactory();
}
catch (HibernateException ex) {
ex.printStackTrace(System.err);
config = null;
factory = null;
}
}

public static Session openSession() throws HibernateException {
Session session = (Session) sessionContext.get();
if (session == null) {
session = factory.openSession();
sessionContext.set(session);
}
return session;
}

public static void closeSession() throws HibernateException {
Session session = (Session) sessionContext.get();
sessionContext.set(null);
if (session != null) {
session.close();
}
}

public static Configuration getConfig() {
return config;
}

public static SessionFactory getFactory() {
return factory;
}

}

有了 ThreadLocalSession 之后,我就可以着手进行 Photos.java, Albums.java 了。

Photos.java:

package annhy.photo.domains;

import java.util.*;

import annhy.photo.hibernate.*;
import net.sf.hibernate.*;

/**
* 用来处理 Photo 的相关 static 函式。
*
* @author Annhy
* @version 1.0, 2003/10/01
*/
public class Photos {
//==================
//== finder methods
//==================
public static Photo findByPK(long id) throws HibernateException {
return findByPK(new Long(id));
}

public static Photo findByPK(Long id) throws HibernateException {
Session s = ThreadLocalSession.openSession();
Photo result = (Photo) s.load(Photo.class, id);
ThreadLocalSession.closeSession();
return result;
}

public static Collection findAll() throws HibernateException {
Session s = ThreadLocalSession.openSession();
Collection result = s.find("from " + Photo.class.getName() + " photo order by photo.id");
ThreadLocalSession.closeSession();
return result;
}
}

Albums.java:

package annhy.photo.domains;

import java.util.*;

import annhy.photo.hibernate.*;
import net.sf.hibernate.*;

/**
* 用来处理 Album 的相关 static 函式。
*
* @author Annhy
* @version 1.0, 2003/10/01
*/
public class Albums {
//==================
//== finder methods
//==================
public static Album findByPK(long id) throws HibernateException {
return findByPK(new Long(id));
}

public static Album findByPK(Long id) throws HibernateException {
Session s = ThreadLocalSession.openSession();
Album result = (Album) s.load(Album.class, id);
ThreadLocalSession.closeSession();
return result;
}

public static Collection findAll() throws HibernateException {
Session s = ThreadLocalSession.openSession();
Collection result = s.find("from " + Album.class.getName());
ThreadLocalSession.closeSession();
return result;
}
}

目前档案列表:
/annhy/photo/domains/Album.java
/annhy/photo/domains/Albums.java
/annhy/photo/domains/Photo.java
/annhy/photo/domains/Photos.java
/annhy/photo/hibernate/ThreadLocalSession.java
/hibernate.properties
/annhy/photo/domains/Photo.hbm.xml
/annhy/photo/domains/Album.hbm.xml

================================================================================

利用 Hibernate 产生对应的数据库

现在终于可以建立数据库,并且新增测试资料了。

PhotoTester.java:

package annhy.photo;

import annhy.photo.domains.*;
import annhy.photo.hibernate.*;
import net.sf.hibernate.*;
import net.sf.hibernate.tool.hbm2ddl.*;

public class PhotoTester {
public static void main(String[] args) throws HibernateException {
// 产生 Database Schema Script 文件并建立数据库
generateDbSchemaScript(true);

// 建立测试数据
insertTestData();

// 查询测试数据
assert Albums.findAll().size() == 8 : "应该有 8 个 Album";
assert Photos.findAll().size() == 3 : "应该有 3 个 Photo";
assert Photos.findByPK(1).getAlbums().size() == 1 : "Photo 1 归属于 1 个相簿";
assert Photos.findByPK(3).getAlbums().size() == 2 : "Photo 3 归属于 2 个相簿";
}

public static void generateDbSchemaScript(boolean affectToDb) throws HibernateException {
SchemaExport dbExport = new SchemaExport(ThreadLocalSession.getConfig());
dbExport.setOutputFile("Photo_DB_Schema.sql");
dbExport.create(false, affectToDb);
}

public static void insertTestData() throws HibernateException {
Session s = ThreadLocalSession.openSession();
Transaction t = s.beginTransaction();

// create all catalog objects
Album[] c = new Album[8];
c[0] = new Album("我的相簿");
c[1] = new Album("家人");
c[2] = new Album("学生时代");
c[3] = new Album("其它");
c[4] = new Album("亲爱的老婆大人");
c[5] = new Album("宝贝女儿");
c[6] = new Album("大学");
c[7] = new Album("研究所");
for (int i = 0; i < c.length; i++) {
s.save(c[i]);
}

// create the catalog hierarchy
c[0].addChild(c[1]);
c[0].addChild(c[2]);
c[0].addChild(c[3]);
c[1].addChild(c[4]);
c[1].addChild(c[5]);
c[2].addChild(c[6]);
c[2].addChild(c[7]);

// 对整个对象网的 root 储存动作,所有对象的更动都会存入数据库
s.save(c[0]);

// create all photo objects
Photo[] q = new Photo[3];
q[0] = new Photo("c://images//1.jpg");
q[0].setTitle("大学毕业照!!");

q[1] = new Photo("c://images//2.jpg");
q[1].setTitle("研究所毕业照!!");

q[2] = new Photo("c://images//3.jpg");
q[2].setTitle("宝贝女儿满月母女合照");

for (int i = 0; i < q.length; i++) {
s.save(q[i]);
}

// 建立 Photo 与 Album 的关联
c[6].addPhoto(q[0]);
c[7].addPhoto(q[1]);

q[2].addAlbum(c[4]);
q[2].addAlbum(c[5]);

// 再一次储存全部对象
s.save(c[0]);

// 交易完成
t.commit();
ThreadLocalSession.closeSession();
}
}

这里建立测试数据的方式实在很丑,不知道有没有人有比较好的建议。
不过要注意一点,若有两个 domain object 要设定关联性 (ex: obj1.addChild(obj2); ),
则至少其中之一要先用 Hibernate 存入 DB 中才行,不然会有 error。

================================================================================

作者外出取材,敬请 不必 耐心等候..

现在终于懂 少年快报 那些连载漫画作者的心情了...
因为实在是江郎才尽,我真想挂个牌子 作者外出取材,敬请 不必 耐心等候 ,
然后就此消失,避不见面... 这样会不会很恶劣...

目前我还在努力搞懂 Struts 中 ,而且手上的工作快要做不完了 ,
先跟大家介绍我在 SourceForge 上找到的一个 project,
Java Struts-Polls http://sourceforge.net/projects/jpolls 我正在 trace 它的 code,它用到的技巧实在有够多 (虐待我这个新手...),
有兴趣可以一起讨论。
要不是因为它没什么教学文件可看,不然我这个讨论的 thread 就可以关闭了,
把它的教学文件连结过来就好了....

未完待续... but,有空再说了...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: