37.2 Hibernate的下载和安装
37.2.1 下载
具体步骤如下:
(1)如图37.2所示,访问Hibernate官方网站http://www.hibernate.org,单击左边菜单的Download超链接,在下载页面中下载Hibernate Core和Hibernate Tools。前者是Hibernate的核心软件包,后者是一个用于辅助Hibernate开发的Eclipse插件。
图37.2 Hibernate的下载页
(2)直正的下载页面会转到著名开源社区sourceforge.net,如图37.3所示。选择下载ZIP格式压缩包hibernate-3.2.2.ga.zip,解压后的目标结构如图37.4所示。
图37.3 下载文件选择 图37.4 Hibernate解压后的目标结构
主要目录及文件解释:
● hibernate3.jar文件是Hibernate的核心jar包。
● lib目录中有一些Hibernate运行需要依赖的第三方jar包,安装时也要用到。
● src目录中是Hibernate(hibernate3.jar)的源文件。
● etc目录中有一些可以参考的例子文件。
● doc目录中包含Hibernate文档。
37.2.2 安装
1.复制jar包
将解压目录中的hibernate3.jar和lib目录下的jar包,全部复制到项目的Web-INF/lib目录,如果提示有同名文件(commons-logging-1.0.4.jar),覆不覆盖则都一样。其他说明如下:
● 其实并不需要复制lib目录下的所有jar包,本文只是为了安装上的方便。如果在正式发布程序时,希望只包括真正用到的包,则可以参考解压目录lib中的_README.txt,里面有详细描述。或者参考Hibernate文档,里面也有部分描述。
● 注意不要将这些jar包复制到%TOMCAT_HOME%/common/lib目录下,那是Tomcat全局库所在目录,有可能引起包冲突。
● 检查一下lib目录中是否有重复包(不同版本),如有,则只保留一个最新版的包,否则很可能会引起类冲突。因此要把antlr-2.7.2.jar删除,保留antlr-2.7.6.jar。
2.创建log4j.properties
Hibernate用log4j包来做日志输出,这就要求项目中创建一个log4j的配置文件log4j.properties,否则有些运行日志就无法看到(不会影响程序运行),另外Eclipse控制台视图会输出如下两条警告信息。
log4j:WARN No appenders could be found for logger (org.apache.catalina.startup.TldConfig).
log4j:WARN Please initialize the log4j system properly.
如果读者熟悉log4j,可以自己创建log4j.properties,定义自己想要的日志配置。如果不熟悉log4j,可以直接将解压目录etc下的log4j.properties,复制到项目的“Java Resourcess:src”下。注意,由于J2EE透视图拒绝直接复制文件到“Java Resourcess:src”下,所以可以转到Java透视图再复制。
37.3 一个简单的Hibernate实例(V005)
37.3.1 创建Hibernate配置文件:hibernate.cfg.xml
在“Java Resourcess:src”下创建一个hibernate.cfg.xml文件如下。
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- 数据库 -->
<property name="connection.datasource">java:comp/env/jdbc/mysql</property>
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
<property name="show_sql">true</property>
<!-- 打开Hibernate的session自动管理机制 -->
<property name="current_session_context_class">thread</property>
<!-- 把所有*.hbm.xml文件注册在这里 -->
<mapping resource="cn/com/chengang/sms/model/model.hbm.xml"/>
</session-factory>
</hibernate-configuration>
配置说明:
● connection.datasource设定所用的连接池。
● dialect告诉Hibernate使用哪种SQL数据库方言(dialect)。不同数据库的SQL语法都有一些差异,Hibernate会根据设置的方言来适应这些差异。想知道其他数据的方言名称,可以利用Eclipse的代码提示功能,在Java程序中输入“org.hibernate.dialect.”然后按“Alt+/”快捷键。
● show_sql设定在控制台是否显示Hibernate生成的SQL语句,开发期间设为true,便于调试。
● model.hbm.xml是一个XML映射文件,这个文件创建在model目录下(内容将在以后给出)。注意,这里用的是相对路径,cn字串前面是没有“/”的。
● hibernate.cfg.xml还有一种hibernate.properties的写法,在Hibernate的解压目录etc可找到它的例子。两种写法选一种即可,本文选前一种。
● 某些属性对Hibernate的性能影响很大,比如batch_size项设置成0和30,性能相差会有4倍以上。属性会有一个默认值,但如果所开发的项目需要作性能优化,则可根据实际情况来重新设置。如果想了解hibernate.cfg.xml中更多的属性设置,可以参考Hibernate文档的“表3.3 Hibernate配置属性”,那里有属性的说明和建议值,本文不再复述。
37.3.2 创建XML映射文件:model.hbm.xml
Hibernate之所以能够智能地判断实体类和数据表之间的对应关系,就是因为有XML映射文件。本小节先在cn.com.chengang.sms.model包下创建一个名为model.hbm.xml的XML映射文件,其内容如下。
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="cn.com.chengang.sms.model.Grade" table="grade">
<id name="id">
<generator class="identity"/>
</id>
<property name="name"/>
</class>
</hibernate-mapping>
配置说明:
● model.hbm.xml可以任意命名及放置于其他目录下,当然hibernate.cfg.xml文件也要做相应修改。笔者建议将它和它所对应的实体类放在一个包下,并用包名做文件名。
● <class>项定义了实体类和数据表之间的关系:name是实体类(用类全名),table是对应的数据表(表名不分大小写)。可以省略掉table属性,这时默认表名和实体类同名。在model.hbm.xml文件中可以设置多个<class>项,笔者建议将一个包中的所有实体类都集中在一个*.hbm.xml文件中。
● <id>项定义了主键id字段所用的键值生成方法,identity是一种MSSQL、DB2、MySQL通用的主键值生成方法(Oracle不能用identity,可换成sequence)。要了解更多内容,可以查阅Hibernate文档。
● <property>子项定义了实体类和表字段的关联。本例只设置了定义类字段的name属性,还有一个column属性是定义数据库表字段名的,本例没有设置。Hibernate正是通过这里的设置建立起实体类和数据库表之间的字段对应关系。本例没有设置column,则Hibernate会默认为它和name属性同名。假如,想将Grade实体类的name字段对应于数据库表的grade_name字段,并将表字段的长度定义成16、不允许空值,则可以按照如下设置:
<property name="name">
<column name="grade_name" length="16" not-null="true"/>
</property>
● <property>体现Hibernate的友好性:它可以设置得很详细,也可以很简洁,当设置简洁时,Hibernate会采用默认值。要了解更多关于<property>设置的内容,可以参阅Hibernate文档。
37.3.3 创建HibernateUtil类
HibernateUtil类用于得到一个SessionFactory,而SessionFactory可以得到Session。Session是Hibernate中最重要和使用最频繁的一个对象,实体对象都是通过它来和数据库交互。这个Session和JSP的Session不同,但是有一点类似于JDBC的Connection,即Session比Connection包含的内容更多,功能范围更广。在Hibernate的编程中将不会再使用Connection,而是通过Session来和数据库交互。
HibernateUtil类的内容如下:
package cn.com.chengang.sms.db;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
// 实例化一个SessionFactory对象
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
程序说明:
● HibernateUtil可以任意取名。它是一个静态方法类,即类中的方法都是静态方法。
● SessionFactory是一个静态变量,它由static {…}静态代码块来初始化一个实例。注意,static{…}代码块比较特殊,它既不是方法也不是变量。生成一个SessionFactory对象很耗费时间和资源,所以在这里整个Web系统共用一个SessionFactory。而生成一个Session对象的代价很小,在编程中千万不要把Session写成单例模式来进行实例共享,对Session的使用原则是用完就关闭,而且要尽量早关闭。
● 对于旧版本的Hibernate,此类还有将Session保存/剥离到当前线程中的两个方法。但现在已经不需要了,因为这里在新版本的hibernate.cfg.xml中用current_session _context_class属性打开了Hibernate的Session自动管理机制。
37.3.4 创建GradeManager类
GradeManager类似于DbOperate,它主要提供数据库操作方法。在这里编写了向Grade表插入一条记录的方法,以及取出Grade表中id值大于2的所有记录的方法。代码如下:
package cn.com.chengang.sms.db;
public class GradeManager {
public void insertGrade() throws HibernateException {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Grade grade = new Grade();// 生成一个年级对象
grade.setName("高四");
session.save(grade); // 将这个对象保存到数据库
session.getTransaction().commit();
}
public List<Grade> getGrades() throws HibernateException {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
// 创建一个条件查询语句
String hql = "from Grade as g where g.id > :id";
Query query = session.createQuery(hql); // 创建查询对象
query.setInteger("id", 2); // 设置查询参数
List<Grade> result = query.list(); // 从数据库取出数据,并自动封装到List集合中
//也可以三句合为一句:session.createQuery(hql).setInteger("id", 2).list();
session.getTransaction().commit();// 提交
return result;// 返回数据集
}
public void close(){
HibernateUtil.getSessionFactory().close();
}
}
程序说明:
● 在Session中,每个数据库操作都是在一个事务(Transaction)中进行的,这样可以隔离开不同的操作。Hibernate的事务是JDBC事务的更高层次的抽象,它提供了更好的灵活性和适应性。
● 无论session.getTransaction().commit()提交,或session.getTransaction().rollback()回滚都会自动关闭session。
● 以前都是将实体对象的字段一个个拆散并组合成SQL语句,或者从数据库取出数据后将字段值一个个封装到实体对象。现在用了Hibernate,就再也不必这么处理数据了,这是Hibernate有魅力的一面。
● getGrades方法中用到的hql字串不是JDBC的SQL语句,而是Hibernate自有的HQL语句。关于HQL的更多内容,可查阅Hibernate文档。
37.3.5 创建hibernateTest.jsp
hibernateTest.jsp使用GradeManager类来获得数据,并将数据显示在页面上。代码如下:
<%@ page contentType="text/html; charset=utf8"%>
<%@ page import="cn.com.chengang.sms.db.GradeManager"%>
<%@ page import="cn.com.chengang.sms.model.Grade"%>
<%
GradeManager mgr = new GradeManager();
//插入一个年级对象
mgr.insertGrade();
//取出所有年级对象,并显示在页面上
for (Grade g : mgr.getGrades())
out.print(g.getId() + " " + g.getName() + "</br>");
mgr.close();
%>
37.3.6 总结及实践建议
以上5步完成了一个简单的Hibernate实例,用浏览器运行hibernateTest.jsp的效果如 图37.5所示。
图37.5 hibernateTest.jsp的运行效果
本节用从底层到高层的次序来完成了一个实例的讲解,它虽然简单,但也反映了一个典型Hibernate程序的编写框架:
● HibernateUtil一经完成之后即可系统通用,以后很少改动。
● XML映射文件是项目前期要做的最重要的工作,在项目开发后期就很少会改动它。对初学者来说,编写XML映射文件也是难点所在。
● GradeManager是数据库操作类,这相当于以前的DbOperate类的功能。但它不再直接操作数据库,而是通过Hibernate去访问,所以在GradeManager类中看不到涉及JDBC的代码。
● 最后,就是位于最高层的JSP文件hibernateTest.jsp,这个文件主要使用GradeManager类来操作数据库。
实践建议:
● Lomboz支持XML映射文件的热修改,当XML映射文件改动之后,Lomboz会将它重新装入。不过这可能需要一两秒钟时间,具体时间要视读者所用电脑性能 而定。
● 在Java编程中要时刻注意区分大小写,这是编程中出错较多的原因。
37.4 继续深入使用Hibernate(V006)
在Hibernate中最主要的就是写XML映射文件和HQL查询语句。HQL和SQL相似,有过SQL经验的人可以很轻松地掌握HQL,所以Hibernate的难点集中在XML映射文件的编写上。为了让读者从实例中迅速掌握Hibernate的核心知识,本节将用Hibernate的知识来继续改写原有的用户登录程序。
37.4.1 修改XML映射文件
在本步将把年级、班级、课程和用户4个实体类和表的映射关系设置清楚,这其中涉及多对一关系、一对多关系、继承式实体类(用户类)等知识,关于用户类的设计方案及代码,可参阅26.3节。
给出XML映射文件model.hbm.xml的完整内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!--年级-->
<class name="cn.com.chengang.sms.model.Grade">
<id name="id">
<generator class="identity"/>
</id>
<property name="name"/>
</class>
<!--班级-->
<class name="cn.com.chengang.sms.model.SchoolClass">
<id name="id">
<generator class="identity"/>
</id>
<property name="name"/>
<many-to-one name="grade" class="cn.com.chengang.sms.model.Grade"
column="grade_id" update="false" insert="false" />
</class>
<!--课程-->
<class name="cn.com.chengang.sms.model.Course">
<id name="id">
<generator class="identity"/>
</id>
<property name="name"/>
</class>
<!--用户-->
<class name="cn.com.chengang.sms.model.IUser" discriminator-value="I">
<id name="id">
<generator class="identity"/>
</id>
<discriminator column="type" type="character"/>
<property name="userId"/>
<property name="password"/>
<property name="name"/>
<property name="latestOnline"/>
<!--用户.学生-->
<subclass name="cn.com.chengang.sms.model.Student" discriminator-value="B">
<many-to-one name="schoolclass"
class="cn.com.chengang.sms.model.SchoolClass" column="schoolclass_id"/>
</subclass>
<!--用户.老师-->
<subclass name="cn.com.chengang.sms.model.Teacher" discriminator-value="A">
<set name="courses" table="iuser_course" inverse="true" lazy="true">
<key column="iuser_id"/>
<many-to-many class="cn.com.chengang.sms.model.Course"
column="course_id"/>
</set>
</subclass>
</class>
</hibernate-mapping>
代码说明:
(1)初学者在设置XML映射文件时常常被一对多、多对一等关系搞得很迷糊,感觉这些关系的设置很难捉摸。这里给出一个重要的口诀,即“以实体类的字段为依据来配置XML映射文件:类的字段有则映射有、类的字段无则映射无”。
(2)年级实体类的设置中,把原来的table="grade"去掉了,没有table属性,则默认为表和类同名。这也是当初创建表时为什么用类名做表名的原因:一是名称相同方便记忆,二是配置XML映射文件时也方便简洁。
(3)班级实体类的第三个字段“grade”是年级实体类的类型,一个年级有多个班级,所以班级对年级是多对一的关系。设置多对一关系的字段不再用<property>项,而是用<many-to-one>项来设定,如下:
<many-to-one name="grade" class="cn.com.chengang.sms.model.Grade" column="grade_id"
update="false" insert="false" />
其中name属性是班级实体类中的字段名grade;class属性是grade字段的类型(全类名);column属性对应班级数据表的字段名。
update、insert两项属性的默认值是true,本处设为false。如果设成true,则Hibernate在更新班级实体对象所对应的数据库数据的同时,也会自动更新年级表数据。但一般都不会通过班级实体对象来自动更新年级表,因为年级表基本是不会动的,这种更新不仅没什么用处,反而影响效率。注意,设为false之后,并不是指不能更新年级表,而是指不通过班级实体来自动更新年级表。
有些读者在这里也许会问:“班级对年级是多对一关系,反过来,年级对班级就是一对多关系。为什么只在设置班级实体时,指定了<many-to-one>关系,而在设置年级实体时,没有反过来指定<one-to-many>关系呢?”
年级对班级是一对多关系,没错。但正如前面所说的口诀,年级实体类中并没有指向班级的字段,所以在XML映射中就不设置它的一对多关系。假设在年级实体类中增加一个字段“private List schoolClasses”,用来存放年级下的所有班级,这时就应该用<one-to-many>来设置这种一对多关系了,否则班级记录就不会自动通过Hibernate被抓取到schoolClasses字段中。
(4)用户类是典型的继承式实体类,它的XML映射设置如下:
● <class>项定义接口IUser时多加了一个discriminator-value="I"。
● <discriminator column="type" type="character"/>是指增加一个名为type的character型字段,这个字段用来区分不同的子类。
● 在设置IUser时定义好共同字段。
● 在<subclass>项定义各子类的独有字段,其中discriminator-value属性要求各不相同,此值将存入表的type字段中。
关于这种继承式实体设置的更多内容,可参阅Hibernate文档的“第9章 继承映射”,此文档中共有3种方案,本文选择的是“所有用户类合用一个表”的第一种方案。在本书第26章设计用户表时,也提到了这3种方案,大家可以参照阅读,以便加深理解。
(5)学生类有一个班级字段,学生和班级是多对一关系,所以也用了<many-to-one>来设置此字段。
(6)老师类的设置,代码如下:
<set name="courses" table="iuser_course" inverse="true" lazy="true">
<key column="iuser_id"/>
<many-to-many class="cn.com.chengang.sms.model.Course" column="course_id"/>
</set>
● 老师类和课程是典型的多对多关系:一个老师可以教多门课程,一门课程也可以由多名老师来教。
● 因为老师类中的课程字段被定义成一个Set集合,所以用<set>项来定义,除此之外还有<list>、<map>、<array>等。
● name="courses"对应于老师类字段courses。
● 多对多需要一个新表(仅两字段)来保存两者之间的关联,table="iuser_course"就是这个新表的表名。
● inverse="true",设置反转。因为多对多有两端,当两端同时都做了修改时,Hibernate需要根据inverse项的设置来判断应该依据哪一端来做更新操作。如果两端都同时设为inverse="true",或同时省略inverse,极可能导致更新冲突,所以一般是任选一端(且仅一端)来设置inverse="true"。
● lazy="true"(默认false),设置延迟(也称懒加载)。
延迟是Hibernate中非常重要的概念,主要在设置多对多、一对多关系中使用。比如,要显示一个仅有老师名称的列表,如果没有设置课程字段延迟获取,那么Hibernate会将老师对应的课程记录一并从数据库取出,这样就有点浪费了;如果设置了延迟,那么Hibernate会在真正用到课程记录时,才会去数据库中取,这样就显得智能一些。
但延迟有缺点:①它要求在使用完延迟型数据(如课程数据)之前,Session不能关闭。②如果一个页面肯定要显示所有课程记录,这时不延迟一次取完,要比延迟分批取的效率高得多。
所以使用延迟要平衡利弊,根据实际情况作出选择。以本例来说明,如果老师对应的课程记录特别多,也就是说一次性取完相应课程记录的代价很大,那么应该选择延迟。但本例中,课程表极小(对于中学来说,也就10门课程左右),每个老师对应的课程数很少(一个老师一般只教一门课程,除非是一人全包的乡村小学老师),这时完全可以不用延迟。本例使用延迟仅仅是为了演示。
如果已经设置了延迟,但在某些特殊情况下,又需要提前关闭Session,这时就要提前把所有延迟加载数据一起取出。实现代码如下:
Hibernate.initialize(user.getCourses()); //强行加载延迟字段的数据
session.close(); //关闭session
Set<Course> set=user.getCourses();//在session关闭之后依然可以取得延迟数据
37.4.2 数据库操作类的实现
Hibernate的一个优点就是抽象于数据库,能够适应多数据库,所以过去数据层的设计也就没有必要存在了。把MysqlOperate、OracleOperate、SqlServerOperate、AbstractDbOperate、ConnectManager和SmsFactory 6个类都删除掉,还有SMS类中的无用常量也可以删除掉,仅留CURRENT_USER常量。然后将DbOperate由接口类改成普通类,其具体代码如下:
package cn.com.chengang.sms.db;
import java.util.Collections;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import cn.com.chengang.sms.model.IUser;
public class DbOperate {
// 根据用户名得到用户对象,如返回null则表示此用户不存在
public IUser getUser(String userId) throws RuntimeException {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
IUser user = null;
try {
session.beginTransaction();
user = getUser(session, userId);
session.getTransaction().commit();
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException(e);
}
return user;
}
public IUser getUser(Session session, String userId) {
Query q=session.createQuery("from "+IUser.class.getName()+ " where userId=:userId");
q.setParameter("userId", userId);
List list = q.list();
if (list.isEmpty())
return null;
else
return (IUser) list.get(0);
}
// 根据分页信息对象QueryInfo得到应用的用户记录
// 要求QueryInfo中已有currentPage和pageSize的数据
public List<IUser> getUsers(QueryInfo qi) throws RuntimeException {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
List<IUser> list = null;
try {
session.beginTransaction();
list = getUsers(session, qi);
session.getTransaction().commit();
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException(e);
}
return list;
}
public List<IUser> getUsers(Session session, QueryInfo qi) {
// 得到总记录数
String hql = "select count(*) from " + IUser.class.getName();
qi.rsCount = ((Long) session.createQuery(hql).iterate().next()).intValue();
if (qi.rsCount == 0)// 等于0表示没有记录
return Collections.emptyList();
// 算出总页数
if (qi.rsCount % qi.pageSize == 0)
qi.pageCount = qi.rsCount / qi.pageSize;
else
qi.pageCount = (qi.rsCount / qi.pageSize) + 1;
// 算出起始位置= (当前页号-1)*每页记录数
int start = (qi.currentPage - 1) * qi.pageSize;
Query q = session.createQuery("from " + IUser.class.getName());
q.setFirstResult(start);
q.setMaxResults(qi.pageSize);
return (List<IUser>)q.list();
}
}
程序说明:
● 因为用户的老师类的课程字段采用了延迟,在前面已经讨论过,在获取延迟字段的数据之前不能关闭Session,所以取得用户的方法分成了两种:带Session参数的和不带的。如果不会用到课程数据,就用不带Session的方法,方便一些。
● 在第二种方法中的IUser.class.getName()得到的是IUser的全类名“cn.com.chengang. sms.model.IUser”,在HQL查询中,不用在查询之前加“select *”字串,而且表名是用类全名来代替,这也体现了Hibernate面向对象的风格。
● 第三种方法是分页式取数据的方法,q.setFirstResult(start)是设置起始记录位置,q.setMaxResults(qi.pageSize)是设置本次要取的记录数。
● 在这里不仅捕获了Exception异常,并且在处理完异常后,再次将该异常包装成RuntimeException异常抛出。这是一种较标准的写法,是为了在类外程序中使用该方法时,能够再次捕获异常,并作一些处理。如果要做得更完善,则可以专门继承RuntimeException创建一个子异常类给DbOperate类用。
37.4.3 修改使用DbOperate类的程序
1.LogonAction类
此类代码基本不变,仅将原来取数据的两行代码
DbOperate db = SmsFactory.getDbOperate();
IUser user = db.getUser(userId);
更改为:
IUser user=new DbOperate().getUser(userId);
2.对显示用户列表userList.jsp文件修改示意如下(主要改动用粗体字标示)
<%@ page contentType="text/html; charset=utf8"%>
<%@ page import="cn.com.chengang.sms.model.*"%>
<%@ page import="cn.com.chengang.sms.db.*"%>
<%@ page import="java.util.*"%>
<%@ page import="org.hibernate.Session"%>
<%@ include file="../checkLogon.jsp"%>
<HTML>
<HEAD><TITLE>用户列表</TITLE></HEAD>
<BODY><table>
<tr><td>ID</td><td>用户名</td><td>密码</td><td>姓名</td><td>班级</td><td>课程</td><td>最后登录时间</td></tr>
<%
QueryInfo qi=new QueryInfo();
qi.currentPage=1; //显示第一页
qi.pageSize=100; //每页100条记录
Session hsession = HibernateUtil.getSessionFactory().getCurrentSession();
hsession.beginTransaction();
try{
List<IUser> list = new DbOperate().getUsers(hsession,qi);
for (IUser user:list) {%>
<tr>
<td><%=user.getId()%></td>
<td><%=user.getUserId()%></td>
<td><%=user.getPassword()%></td>
<td><%=user.getName()%></td>
<td>
<%if (user instanceof Student) {
SchoolClass s = ((Student) user).getSchoolclass();
if (s != null)
out.print(s.getName());
}%>
</td>
<td>
<%if (user instanceof Teacher) {
Set<Course> set = ((Teacher) user).getCourses();
StringBuilder sb=new StringBuilder();
for (Iterator it = set.iterator(); it.hasNext();) {
Course course = (Course) it.next();
sb.append(course.getName());
if (it.hasNext())
sb.append(", ");
}
out.print(sb.toString());
}%>
</td>
<td><%=user.getLatestOnline()%></td>
</tr>
<%
}
hsession.getTransaction().commit();
} catch (Exception e) {
hsession.getTransaction().rollback();
throw new RuntimeException(e);
}
%>
</table></BODY></HTML>
程序说明:
● 在文件头增加了一个<%@ page import来引用hibernate包。
● 将Session对象取名hsession是为了避免和JSP的默认对象Session产生同名冲突。
● hsession必须要在取数据循环结束后才能关闭,因为老师类的课程字段用的是延迟获取数据的方式。
37.5 实现用户的修改、删除功能(V007)
37.5.1 界面效果及功能说明
本节将同时需要Struts和Hibernate的知识,实例完成后的用户列表页面如图37.6所示。
● 单击“删除”超链接后,不提问,直接从数据库中删除记录,并再次返回用户列表页面。
● 单击图37.6中的“修改”超链接后,打开如图37.7所示的页面。
● 单击图37.7中的“修改”按钮后,再次返回用户列表页面,此时页面显示新值。
图37.6 用户列表页面 图37.7 修改页面
37.5.2 在DbOperate类增加方法
在DbOperate类分别加入删除数据、插入更新数据的方法,每种方法都提供带Session参数和不带Session参数的两种。saveOrUpdate同时具有插入和更新的功能,从这一点也可以体现Hibernate的高度智能化,的确大大简化了操作数据表的工作。除了本例通过一个查询HQL来删除,还有一种常用的传入实体类的删除方法:“session.delete(Object obj)”,Hibernate会根据实体类型去相应的表中将记录删除。
加入到DbOperate类的代码如下:
// 用HQL语法来删除数据,参数hql是一个查询字符串
public void delete(String hql) throws RuntimeException {
Session session = HibernateUtil.getSessionFactory().openSession();
try {
session.beginTransaction();
delete(session, hql);
session.getTransaction().commit();
System.err.println("try "+session.isOpen()+"_"+session.isConnected());
} catch (Exception e) {
session.getTransaction().rollback();
System.err.println("Exception "+session.isOpen()+"_"+session.isConnected());
throw new RuntimeException(e);
}finally{
session.close();
System.err.println("finally "+session.isOpen()+"_"+session.isConnected());
}
}
public void delete(Session session, String hql) {
session.createQuery(hql).executeUpdate();
}
// 插入或更新实体对象所对应的记录
public void saveOrUpdate(Object obj) throws RuntimeException {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
try {
session.beginTransaction();
saveOrUpdate(session, obj);
session.getTransaction().commit();
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException(e);
}
}
public void saveOrUpdate(Session session, Object obj) {
session.saveOrUpdate(obj);
}
37.5.3 在用户列表userList.jsp文件增加两个超链接
在原有的userList.jsp的每条记录后面加入“修改”、“删除”超链接,代码如下:
<td><A HREF="/myweb/user/userAction.do?method=showUser&userId=<%=user.getUserId()%>"
>修改</A></td>
<td><A HREF="/myweb/user/userAction.do?method=removeUser&id=<%=user.getId()%>"
>删除</A></td>
(1)以用户chen为例,给出相应的IE地址显示如下:
修改的IE地址显示:
http://localhost:8080/myweb/user/userAction.do?method=showUser&userId=chen
删除的IE地址显示:
http://localhost:8080/myweb/user/userAction.do?method=removeUser&id=1
(2)在这里用的UserAction稍后创建,它继承自Struts的DispatchAction类,并有3种方法removeUser、showUser和modifyUser分别对应于删除、显示和修改。
(3)要修改用户,首先得把它的值显示出来,所以method参数值为showUser。修改使用userId参数(用户名),删除使用id参数(自动递增主键)。当然,也可以改为通过userId来删除记录。
(4)特别强调:
● 超链接的HREF属性和表单标签(<html:form)的action属性在地址定位上有区别,表单action定位用/user/action.do,而超链接HREF定位用/myweb/user/action.do或action.do。这是比较容易忽略的一点。
● method参数的值必须是UserAction中的一个方法名。
● userId、id两个参数必须是UserAction对应的UserForm类中的字段。
37.5.4 在Struts配置文件struts-config.xml中增加一个action定义
将下面的XML块,加入到<action-mappings>…</action-mappings>之间。
<action path="/user/userAction" type="cn.com.chengang.sms.user.UserAction"
name="userForm" scope="request" validate="true"
input="/user/userList.jsp" parameter="method">
<forward name="modifyUserView" path="/user/modifyUser.jsp"/>
<forward name="modifySuccess" path="/user/userList.jsp"/>
</action>
代码说明:
● <action>项的path属性定义了UserAction。
● parameter属性是DispatchAction型Action所必需的。
● name="userForm",此项设置说明UserAction也是用userForm来封装表单数据,当然,userForm还要加入一些字段,在后面会给出其代码。
● 这里设置了两个转发:一个是修改页面的modifyUserView;一个是修改成功后的返回页面modifySuccess。
37.5.5 修改UserForm类
UserForm类需要做如下3处修改:
(1)由于修改页面和登录页面合用UserForm类,所以要向类中再多加入修改页面用到的3个字段(同时还有相应的Setter/Getter方法),代码如下:
private Long id; //数据库ID
private String name; //姓名
private Date latestOnline;//最后登录时间
//-----Setter/Getter方法,省略----
(2)因为UserAction继承自DispatchAction类,这种类型的Action要求UserForm继承ValidatorActionForm,否则无法使用36.5.2节方法2中的验证方式。将UserForm的父类改为ValidatorActionForm。代码如下:
public class UserForm extends ValidatorActionForm {……
(3)最后还必须将UserForm中的validate方法删除,否则修改用户页面无法被打开。
从这里可以看到Struts程序的代码复用率很高,共用一个UserForm,这不仅减少了创建ActionForm的个数,还能共用验证机制。Struts允许将系统中所有表单用到的字段合在一个ActionForm中,但不推荐这样做。也有人认为应该一个JSP表单对应一个ActionForm,但这种方案的代码复用率太低。
建议将同类型表单所用到的字段合在一个ActionForm中,就像UserForm一样。这样既不会产生太多ActionForm,又有很好的代码复用。当然,这里面没有绝对的法则,读者应该根据自己实际的开发情况来选择。
37.5.6 创建UserAction类
创建UserAction类的代码如下:
package cn.com.chengang.sms.user;
public class UserAction extends DispatchAction {
private final static String USER = "modiUser";
// 删除用户的Action
public ActionForward removeUser(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) throws Exception {
UserForm actionForm = (UserForm) form;
Long id = actionForm.getId();
String hql = "delete " + IUser.class.getName() + " where id=" + id;
new DbOperate().delete(hql);
return (mapping.getInputForward());
}
// 修改用户的Action
public ActionForward modifyUser(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) throws Exception {
UserForm actionForm = (UserForm) form;
IUser user = (IUser) request.getSession().getAttribute(USER);
request.getSession().removeAttribute(USER);
user.setUserId(actionForm.getUserId());
user.setPassword(actionForm.getPassword());
user.setName(actionForm.getName());
new DbOperate().saveOrUpdate(user);// 更新数据库
return (mapping.findForward("modifySuccess"));
}
// 显示一个用户的Action
public ActionForward showUser(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) throws Exception {
UserForm actionForm = (UserForm) form;
String userId = actionForm.getUserId();
IUser user = new DbOperate().getUser(userId);
actionForm.setUserId(user.getUserId());
actionForm.setPassword(user.getPassword());
actionForm.setName(user.getName());
actionForm.setLatestOnline(user.getLatestOnline());
request.getSession().setAttribute(USER, user);// 保存用户状态
return (mapping.findForward("modifyUserView"));
}
}
程序说明:
(1)在显示用户的方法showUser中,先取得用户对象,然后再将要显示在界面上的值转给UserForm。在修改用户的页面中,UserForm的字段值会自动显示在相应的文本框中。
(2)在程序中定义了一个字符串常量USER。如果要经常用到某个字符串,最好定义成常量,否则容易因为书写错误而产生难以发现的BUG。
(3)可以通过new DbOperate().saveOrUpdate(user)来直接更新user代表的数据记录,但这要求让user对象从showUser中取出后,到modifyUser中还能用。本例采用HttpSession来维持user的状态。
HttpSession是保存在服务器端的,通常HttpSession是30分钟左右失效(这是可设置的),HttpSession本身所占内存并不大,但在此期间,因为它保持着对user对象的引用,这使得user对象及user引用的其他对象都无法被JVM垃圾回收器回收。所以在使用完HttpSession之后,要记得用removeAttribute(USER)方法将HttpSession对user的引用清除。
对于user对象状态的保持,可能还会有以下几种行不通的想法:
● 用request来保持状态。
分析:这是不行的,user状态可从showUser方法保持到modifyUser.jsp页面,但在modifyUser.jsp页面提交修改后,转到modifyUser方法中时,user对象已失效。
● 在UserForm创建一个user字段。
分析:user状态一样无法保持到modifyUser方法中,当提交表单转到modifyUser时,所有在表单中没有被设置的UserForm字段值都会被清空。
● 在UserAction类创建一个user字段来保存user对象。
分析:这是错误的。Struts的机制是:UserAction在整个系统只有一个实例,因此UserAction类的user字段会被所有用户线程共用,这将导致很严重的BUG。
37.5.7 创建modifyUser.jsp
modifyUser.jsp的作用是用文本框显示用户数据,当修改完成后,单击“修改”按钮即可将这些修改提交到数据库中。代码如下:
<%@ page contentType="text/html; charset=utf8"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>
<HTML><HEAD><TITLE>修改用户</TITLE>
<META http-equiv=Content-Type content="text/html; charset=utf8">
</HEAD><BODY>
<html:form action="/user/userAction.do?method=modifyUser" focus="userId" οnsubmit="return validateUserForm(this)">
<table width="100%">
<tr><td>
用户名:<html:text property="userId" maxlength="10" />
<font color="red"><html:errors property="userId"/></font>
</td></tr>
<tr><td>
密 码:<html:text property="password" maxlength="20"/>
<font color="red"><html:errors property="password"/></font>
</td></tr>
<tr><td>
姓 名:<html:text property="name" maxlength="20"/>
<font color="red"><html:errors property="name"/></font>
</td></tr>
<tr><td>
最后登录时间:
<bean:write name="userForm" property="latestOnline"/>
</td></tr>
<tr><td>
<html:submit>修改</html:submit><html:reset>重填</html:reset>
</td></tr>
</table>
</html:form>
<html:javascript dynamicJavascript="true" staticJavascript="true" formName="userForm"/>
</BODY></HTML>
程序说明:
● 在页头加入了html和bean标签的声明,并在最后登录时间值的输出时使用了<bean:write >标签。在<bean>标签中的name属性引用了userForm对象,userForm对象是Struts在JSP页面中隐式生成的。
● 此页面和logon.jsp是非常相似的,特别是对值进行验证机制两者完全一样。