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

写在前面

最近开始学 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,有空再说了...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值