本章介绍了Hibernate的几种主要检索方式:HQL检索方式、QBC检索方式、SQL检索方式。HQL是Hibernate Query Language的缩写,是官方推荐的查询语言。QBC是Query By Criteria的缩写,是Hibernate提供的一个查询接口。Hibernate是一个轻量级的框架,它允许使用原始SQL语句查询数据库。
6.1 HQL基础
HQL是Hiberante官方推荐的Hibernate检索方式,它使用类似SQL的查询语言,以面向对象的方式从数据库中查询。可以使用HQL查询具有继承、多态和关联关系的数据。在检索数据时应优先考虑使用HQL方式。
6.1.1 默认数据库表和数据
在讲解本章时,在没有特殊说明时,用到的数据库均为joblog,也就是在第4章建立的数据库。joblog中添加了3个表:学生表student、课程表course和选课表sc。
学生表student中各字段的结构如图6-1所示,字段的中文含义在Comment列中。
图6-1 学生表的数据结构
学生表student中的数据如图6-2所示,没有特殊说明时,用到的均为这6条记录。
此处仍然使用在第4章建立的HibernateProject项目,但是这里新建了一个包hibernate.ch06,这个包存放本章中的所有代码。在hibernate.ch06包中建立学生表对应的持久化类Student.java,代码如下。
图6-2 学生表中的数据
package hibernate.ch06;
//学生类
public class Student {
private Integer id; //对象标识符
private Integer sno; //学号
private String sname; //姓名
private String ssex; //性别
private String sdept; //所在系别
private Integer sage; //年龄
private String saddress; //籍贯
…… //省略了所有的get/set访问器
}
课程表course中的各个字段的结构如图6-3所示,字段的中文含义在Comment列中。
图6-3 课程表的结构
课程表中的数据如图6-4所示,如果没有特殊说明,用到的均为这4条记录。
图6-4 课程表的数据
在hibernate.ch06中新建持久化类Course.java类,代码如下。
package hibernate.ch06;
//课程类
public class Course {
private Integer id; //对象标识符
private Integer cno; //课程号
private String cname; //课程名
private Integer Ccredit; //学分
…… //省略了get/set访问器
}
选修表sc(sc为student-course的缩写)的结构如图6-5所示,字段的中文含义在Comment列中。
图6-5 选修表的结构
选修表中的数据如图6-6所示,没有特殊说明时,用到的均为这5条记录。
图6-6 选修表的数据
在hibernate.ch06中新建持久化类SC.java,SC.java的代码如下。
package hibernate.ch06;
//选课类
public class SC implements java.io.Serializable {
private Integer id; //id
private Integer sno; //学号
private Integer cno; //课程号
private Integer grade; //成绩
public SC() {
}
…… //省略get/set访问器
}
后面的章节中将用这3个表和3个持久化类进行讲解。
6.1.2 检索类的所有对象
使用HQL语句可以检索出一个类的所有对象,如HQL语句“from Student”表示检索Student类的所有对象。下面的程序检索学生类的所有对象。
Query query=session.createQuery("from Student"); //创建Query对象
List list=query.list(); //执行查询
//以下代码做显示用,以后不再写出来
Iterator it=list.iterator();
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println("id"+stu.getId());
System.out.println("name"+stu.getSname());
System.out.println("\n");
}
session.createQuery()以HQL查询语句为参数,生成一个查询对象。本例中的HQL语句为“from Student”,这是from子句,格式如下。
from 类名
其中,类名可以为类的全限定名,如:
from hibernate.ch06.Student
Hibernate使用自动引入功能(auto import),会自动寻找需要的类,所以不推荐使用类的全限定名。注意,类名区分大小写,如果写成from student,将会抛出以下异常。
java.lang.NoClassDefFoundError: hibernate/ch06/student (wrong name: hibernate/ch06/Student)
HQL关键字不区分大小写,FROM、from和From是一样的。
调用query.list()时,真正开始执行HQL查询语句,并把查询的结果放在List中。
本例中查询的是Student类中的所有属性,如果查询Student类中的某一个或某几个属性,如查询所有学生的姓名和所在系,需要用到属性查询。
6.1.3 检索类的某几个属性
与SQL语句类似,HQL语句可以检索类的某一个或者某几个属性。以下代码查询所有学生的姓名和所在系。
//创建Query对象
Query query=session.createQuery("select Student.sname,Student.sdept from Student");
List list=query.list(); //执行查询
//以下代码显示查询的信息
Iterator it=list.iterator();
while(it.hasNext()){
Object[] stu=(Object[])it.next();
System.out.println("id"+stu[0]);
System.out.println("name"+stu[1]);
System.out.println("\n");
}
属性查询使用select关键字,属性查询的格式如下。
select 属性1,属性2,… from 类名
属性前可以加上类名加以限定,如:
select 属性1,属性2,… from 类名
但一般没有必要。
属性查询区分大小写,上面的代码中如果写成:
select SNAME,Sdept from Student
将抛出异常,提示找不到属性SNAME和属性Sdept。
查询结果将只显示查询的属性列。
属性查询的结果,对于用it.next()获得的每条记录,可以存储在Object[]数组中,以便进行存取。
6.1.4 指定别名
在查询时,可以用关键字as指定查询的别名,指定别名可以简化查询,有时必需指定别名才能进行查询。以下代码查询学号中含有4的学生的姓名和所在系。
select s.sname,s.sdept from Student as s where s.sno like '%4%'from Student s
s就是类Student的别名。注意as可以省略,即下面的查询语句和上面的语句是等效的。
select s.sname,s.sdept from Student s where s.sno like '%4%'from Student s
6.1.5 where条件子句
where条件子句跟SQL中的where条件子句类似,它检索符合条件的对象。例如,查询所有所在系别为计算机系的学生:
select s.sname,s.sdept from Student s where s.dept=’计算机’
where子句指定查询的条件,其语法和SQL类似。
在where子句中可以指定比较运算符:>、>=、<、<=、<>,其含义分别为大于、大于等于、小于、小于等于、不等于。
查询年龄在22到23岁的学生:
from Student s where s.sage>=22 and s.sage<=23
在where子句中指定查询的属性是否为null:is null、is not null,其含义分别表示为空和不为空。
查询所在籍贯为空的学生:
from Student s where s.saddress is null
6.1.6 使用distinct过滤掉重复值
使用distinct关键字将去掉结果中的重复值,只检索符合条件的对象。如下面的例子检索学生实例中的不重复的年龄。
Session session=HibernateSessionFactory.currentSession(); //创建Session
String hql="select distinct s.sage from Student s"; //HQL查询语句
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
检索的结果如下,可见结果中去掉了一个重复的22岁。
20
21
22
23
24
6.1.7 删除对象
HQL语句可以直接对复合条件的对象进行删除,可以指定删除的对象,并在提交后永久持久化到数据库。HQL使用delete进行删除,如删除年龄大于25岁的学生可以使用如下代码。
Session session=HibernateSessionFactory.currentSession(); //创建Session
Transaction tx=null; //声明事务
try{
tx=session.beginTransaction(); //开始事务
//创建查询
String hql="delete Student s where s.sage>25";
Query query=session.createQuery(hql);
query.executeUpdate(); //执行
tx.commit(); //成功,则提交
tx=null;
}catch(Exception e){
e.printStackTrace();
if(tx!=null){
tx.rollback(); //失败则回滚
}
}finally{
session.close();
}
注意以下两点。
在删除对象时,执行query.executeUpdate()进行数据删除,但只有执行了tx.commit()进行事务提交时,才真正从数据库中删除数据。
如果设置了级联删除,则与之相关联的对象实例也被删除。
6.1.8 更新对象值
更新对象的HQL语句与SQL语法很相似,使用update更新对象的值。如下面例子更新对象的sage属性。
ransaction tx=null; //声明事务
try{
tx=session.beginTransaction(); //开始事务
String hql="update Student s set s.sage='22' where s.id=11"; //更新语句
Query query=session.createQuery(hql);
query.executeUpdate(); //执行
tx.commit(); //成功,则提交
tx=null;
}catch(Exception e){
e.printStackTrace();
if(tx!=null){
tx.rollback(); //失败则回滚
}
}finally{
session.close();
}
6.1.9 查询计算属性值
HQL可以查询经过计算的值,在一些需要计算的地方可以进行计算,例如查询全体学生的姓名和出生年份。
select s.sname,2006-s.sage from Student as s
select子句十分灵活,几乎和SQL语句有着同样的能力,对象的属性值可以参与运算。
这行代码假设当前的年份是2006年。
下面是另外几个查询计算属性值的例子。
select s.sname,2006-s.sage from Student as s
6.1.10 使用函数
当需要调用函数时,HQL提供了一些类似SQL的函数。这些函数可以简化操作。例如查询学生的姓名、出生日期和性别,其中性别用小写表示。
select s.sname,2006-s.sage,lower(s.ssex) from Student as sselect s.sname,2006-s. sage from Student as s
6.1.11 between...and...和not between... and...确定查询范围
between...and...用来查询属性值在指定范围内的实体对象,not between...and...用来查询属性值不在指定范围内的实体对象。如查询学生年龄在22到23之间的学生:
select s.sno,s.sname,s.sage from Student s where s.sage between 22 and 23
查询将返回如下结果。
---------------------------------------------------------------------
1 20040001 李晓梅 22 计算机系
---------------------------------------------------------------------
2 20040002 王蒙 23 外语系
---------------------------------------------------------------------
4 20050004 李文 22 计算机系
between后跟的是查询范围的下限,and后跟的是查询范围的上限,所以下面的查询语句总没有对象返回。
from Student s where s.sage between 23 and 22
6.1.12 in和not in确定查询集合
关键字in用来查询指定属性值属于指定集合的对象,关键字not in用来查询指定属性值不属于指定集合的对象。如查询不是计算机系,也不是数学系的学生。
select s.sno,s.sname,s.sdept from Student s where s.sdept not in ('计算机系','数学系')
查询将返回如下结果。
---------------------------------------------------------------------
20040002 王蒙 外语系
---------------------------------------------------------------------
20050003 姜浩 化学系
---------------------------------------------------------------------
20050005 薛鹏 生物系
6.1.13 like进行模糊查询
用like进行模糊查询时有两个可用的通配符:“%”和“_”。“%”代表长度大于等于0的字符,“_”代表长度为1的单个字符。
查询姓李的学生:
select s.sno,s.sname,s.sdept from Student s where s.sname like '%李%'
查询结果如下。
---------------------------------------------------------------------
20040001 李晓梅 计算机系
---------------------------------------------------------------------
20050004 李文 计算机系
---------------------------------------------------------------------
20050006 李思 数学系
查询姓名为两个字符的学生:
select s.sno,s.sname,s.sdept from Student s where s.sname like '__'
查询结果如下。
---------------------------------------------------------------------
20040002 王蒙 外语系
---------------------------------------------------------------------
20050003 姜浩 化学系
---------------------------------------------------------------------
20050004 李文 计算机系
---------------------------------------------------------------------
20050005 薛鹏 生物系
---------------------------------------------------------------------
20050006 李思 数学系
6.1.14 and逻辑与
当要检索指定的多个条件,且条件的逻辑关系为与时,使用“and”关键字。如检索计算机系的女生,这个检索要求包含两个条件:“计算机系”和“女生”。
select s.sno,s.sname,s.sdept from Student s where s.sdept='计算机系' and s.ssex='F'
检索的结果如下。
---------------------------------------------------------------------
20040001 李晓梅 计算机系
---------------------------------------------------------------------
20050004 李文 计算机系
6.1.15 or逻辑或
当检索的多个条件,且条件的逻辑关系为或时,使用“or”关键字。如检索姓王,或者年龄大于22岁的学生:
select s.sno,s.sname,s.sdept from Student s where s.sname like '王%' or s.sage>22
检索结果如下。
---------------------------------------------------------------------
20040002 王蒙 外语系
---------------------------------------------------------------------
20050005 薛鹏 生物系
6.1.16 order by对结果进行排序
“order by”关键字对结果进行排序,默认为升序。“order by asc”为升序,“order by desc”为降序。例如将学生表中的学生按照年龄升序排序:
from Student s order by s.sage
检索结果如下。
---------------------------------------------------------------------
6 20050006 李思 20 数学系
---------------------------------------------------------------------
3 20050003 姜浩 21 化学系
---------------------------------------------------------------------
1 20040001 李晓梅 22 计算机系
---------------------------------------------------------------------
4 20050004 李文 22 计算机系
---------------------------------------------------------------------
2 20040002 王蒙 23 外语系
---------------------------------------------------------------------
5 20050005 薛鹏 24 生物系
将学生表中的学生按照年龄降序排列,按照所在系升序排列。
from Student s order by s.sage,s.sdept desc
6.1.17 group by对记录进行分组
对查询进行分组可以对查询进行细化。分组经常和聚集函数一起使用,这样聚集函数将作用于每个分组。
group by的用法为:
select 属性1,属性2,属性3 ,…,属性n from 类名 group by 属性m
其中属性1,属性2,…,属性n必须满足下列条件。
要么作为聚集函数的参数,要么为属性m。例如,检索各个系的学生的平均年龄:
select avg(s.sage),s.sdept from Student s group by s.sdept
其中字段s.sage作为平均值函数的参数,s.sdept是group by后的一个属性。检索的结果如下。
---------------------------------------------------------------------
化学系 21.0
---------------------------------------------------------------------
计算机系 22.0
---------------------------------------------------------------------
生物系 24.0
---------------------------------------------------------------------
数学系 20.0
---------------------------------------------------------------------
外语系 23.0
检索各个课程号与对应的选课人数。
select cno,count(sno) from SC s group by s.cno
6.1.18 having关键字
having关键字和group by关键字搭配使用,它对分组后的记录进行筛选,输出符合having指定条件的组。例如查询人数超过1000人的系。
select s.sdept from Student s group by s.sdept having count(*)>1000
查询男生人数多于500人的系。
select s.sdept from Student s where s.ssex=’M’ group by s.sdept having count(*)>500
以上面查询男生人数多于500人的系为例:
select s.sdept from Student s where s.ssex=’M’ group by s.sdept having count(*)>500
查询过程中同时使用group by和having关键字时,查询步骤如下。
(1)检索符合s.ssex=‘M’的所有男生。
(2)根据s.sdept分组成不同的系。
(3)对于每一个分组,计算分组中的记录条数大于500的系。
(4)将符合上述条件的s.sdept选出来。
where和having的区别在于作用对象不同。where作用于基本表,而having作用于分组后的组。
6.1.19 聚集函数
聚集函数包括count()、avg()、sum()、max()、min(),其含义如表6-1所示。
表6-1 聚集函数及其含义
聚 集 函 数
含 义
count()
计算符合条件的记录条数
avg()
计算符合条件的平均值
sum()
计算符合条件的和
max()
计算符合条件的最大值
min()
计算符合条件的最小值
各个函数的用法举例如下。
检索学生实例的对象个数:
select count(*) from Student
检索计算机系的人数:
select count(*) from Student s where s.sdept='计算机系'
检索学生实例的平均年龄:
select avg(s.sage) from Student s
检索课程表course的所有课程的学分的总和:
select sum(c.ccredit) from Course c
检索课程号为“1”的课程的最高成绩:
select max(s.grade) from SC s where s.cno=1
检索课程号为“1”的课程的最低成绩:
select min(s.grade) from SC s where s.cno=1
聚集函数经常和group by分组关键字搭配使用。
检索各门课程的平均成绩:
select s.cno,avg(s.grade) from SC s group by s.cno
检索各科不及格人数:
select s.cno,count(*) from SC s where s.grade<60 group by s.cno
6.2 HQL进阶
下面讲述HQL一些比较高级的应用,包括如何使用HQL查询继承关系数据、绑定参数和在配置文件中使用查询语句。
6.2.1 查询类及其所有继承的类的实例
默认情况下,当查询一个类时,Hibernate会自动搜索这个类的所有继承类。假如有如下3个类,类的关系如图6-7所示。
图6-7 Animal类及其子类Bird和Mammal
当调用如下HQL语句时,会查询出所有的Animal实例、Bird实例和Mammal实例。
from Animal
所有的类均继承自java.lang.Object,所以下面的HQL语句查询所有的类的对象实体,即查询所有映射表的记录。
from java.lang.Object
6.2.2 限制每次查询的返回对象数
Query接口提供了两个函数,用于限制每次查询返回的对象数。
SetFirstResult(int firstResult) 用于设置从哪一个对象开始检索。参数firstResult设置开始检索的起始记录。
setMaxResults(int maxResults) 用于设置每次检索返回的最大对象数。参数maxResults用于设置每次检索返回的对象数目。
这两个函数结合起来用,经常用于分页显示对象。例如数据库中有10000条记录,如果一次性显示实在太多,就可以进行分页显示。
下面的程序当每次循环时,从Student实例中检索出pageSize个对象,并输出到控制台,是一个典型的分页显示程序。
/**
* 分页输出对象
* @param pageSize 每页显示的记录条数
*/
public void pagenate(int pageSize)
{
Session session=HibernateSessionFactory.currentSession(); //创建Session
String hql="from Student"; //检索Student实例的HQL语句
String hql1="select count(*) from Student"; //检索出表中有多少条记录的HQL语句
Query q=session.createQuery(hql1); //创建Query
List list=q.list(); //执行查询
int count=((Integer)list.get(0)).intValue();//总的对象个数
int pageCount=(count+pageSize-1)/pageSize; //总的页数
Query query=session.createQuery(hql); //创建检索Student的查询
for(int i=0;i<pageCount;i++){
query.setFirstResult(i*pageSize);
query.setMaxResults(pageSize);
List list1=query.list(); //执行查询
Iterator it=list1.iterator();
System.out.println("***************************");
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println(stu.getId()+"\t"
+stu.getSno()+"\t"
+stu.getSname()+"\t"
+stu.getSsex()+"\t"
+stu.getSage()+"\t"
+stu.getSdept()+"\t"
+stu.getSaddress()
);
}
}
}
pagenate(int pageSize) 函数对指定的对象实例循环查询,每次循环检索出pageSize个对象。
HQL语句select count(*) from Student检索出学生对象的个数,把对象个数存入到变量count中。
用(count+pageSize-1)/pageSize计算出总的页数。
每次for循环时输出一页的记录。
6.2.3 绑定参数
使用绑定参数可以在程序运行时动态决定一些参数的值。下面比较不使用绑定参数和使用绑定参数时的情况。
第4章的4.3.6节中,ViewLog.java中有一个检索指定时间段日志的HQL语句:
String HQL="from Contents where logdate>='"+beginDate+" 00:00:00' and logdate<= '"+endDate+" 00:00:00'";
上述做法存在以下缺陷。
代码的可读性比较差。
安全性问题,用户可能执行别的代码或存在SQL注入式攻击。
性能低下。
查询语句中以“:”开头的变量叫做命名参数,把命名参数和一个具体的值进行绑定的过程叫做绑定参数。如下面的程序。
//声明hql语句,待绑定的参数以":"开头
String hql="from Student where sname=:name and sage>:age"; //name和age为命名参数
Query query=session.createQuery(hql); //创建查询
query.setParameter("name","李晓梅"); //进行绑定
query.setParameter("age",new Integer(20));
List list=query.list(); //执行查询
对上述代码说明如下。
“:name”指定了命名参数name,“:age”指定了命名参数age。
调用Query接口的setParameter()为命名参数赋一固定值。第一个参数指定参数名称,第二个参数指定参数值。当明确知道参数类型时,则可以使用相应的方法,例如参数为int型,可以使用setInteger()方法;参数为String型,可以使用setString()方法。
可以使用按照参数位置对参数进行绑定,如下面的代码所示。
String hql="from Student where sname=? and sage>?"; //声明hql语句,命名参数用“?”代替
Query query=session.createQuery(hql); //创建查询
query.setParameter(0,"李晓梅"); //绑定参数
query.setParameter(1,new Integer(20));
List list=query.list(); //执行查询
在HQL语句中用问号“?”代替命名参数。此时setParameter()函数的第一个参数指定参数的位置(position),0为HQL查询语句中的第一个参数,1为第二个参数,以此类推。
6.2.4 在映射文件配置HQL语句
为了使程序具有更大的灵活性,Hibernate可以在映射文件中配置HQL语句。如下所示为在Student.hbm.xml中的配置。
<hibernate-mapping>
<class name="hibernate.ch06.Student" table="student" catalog="joblog">
<!--此处省略了配置-->
</class>
<query name="searchStudent"><![CDATA[
from Student s where s.sage>22
]]>
</query>
</hibernate-mapping>
可以用如下代码访问配置文件中的HQL语句。
Session session=HibernateSessionFactory.currentSession();//创建Session
Query query=session.getNamedQuery("searchStudent"); //用getNamedQuery得到查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println(stu.getSname());
}
其中,getNamedQuery()函数用来访问映射文件Student.hbm.xml中配置的HQL语句,参数为配置的名称。
6.3 HQL的嵌套子查询
6.3.1 嵌套子查询的概念
在SQL中,一个select-from-where语句成为一个查询块。将一个查询块嵌套在另一个查询块的where子句或having短语的条件中,这样的查询称为嵌套查询或者子查询。如:
from Student s
where s.sno in
(select sno from sc where cno='1')
上面的HQL语句在Hibernate后台生成的SQL语句为:
select
student0_.id as id1_,
student0_.Sno as Sno1_,
student0_.Sname as Sname1_,
student0_.Ssex as Ssex1_,
student0_.Sdept as Sdept1_,
student0_.Sage as Sage1_,
student0_.Saddress as Saddress1_
from
joblog.student student0_
where
student0_.Sno in (
select
sc1_.Sno
from
joblog.sc sc1_
where
sc1_.Cno='1'
)
在这个例子中,下层查询块select sno from sc where cno='1'是嵌套在上层查询块from Student s where s.sno in的where条件中的。上层查询块又称为外层查询或父查询,下层查询块称为内层查询或者子查询。
嵌套查询的求解方法由里向外处理,每一个子查询在其上一级查询处理之前查询,子查询的结果用于建立父查询的查询条件。
6.3.2 带有IN谓词的子查询
带有IN谓词的子查询指的是父查询与子查询用谓词IN连接,判断某个属性列值是否在子查询的结果中。在嵌套查询中,子查询的结果往往是一个集合。
例如,查询与“李晓梅”在同一个系学习的学生。可以使用如下的方法进行:先查询“李晓梅”所在系,然后查询在这个系里的所有学生。先查询的作为条件是子查询,后查询的是父查询。具体代码如下。
from Student s
where s.sdept in
(select s.sdept from s where s.sname='李晓梅')
6.3.3 比较子查询
如果确切知道子查询返回的是单值,可以用=、>、>=、<、<=、<>比较运算符进行比较子查询。
例:查询与“李晓梅”在同一个系学习的学生。
这个例子与上面的例子一样。“李晓梅”只可能在一个系学习,所以子查询返回单值,可以用比较子查询。
from Student s
where s.sdept=
(select s.sdept from s where s.sname='李晓梅')
6.3.4 带有ANY或ALL的子查询
使用ANY或者ALL谓词时,必须同时使用比较运算符。查询其他系中比计算机系任一学生年龄小的学生名单。
from Student s
where s.sage<ANY(select s.sage from s where s.sdept='计算机系')
and s.sdept<>'计算机系'
子查询查询出计算机系学生的所有年龄,然后用“<ANY”关键字进行年龄比较。
and后的条件s.sdept<>‘计算机系’是父查询的条件。
带有ANY或ALL的子查询的谓词如下所述。
>ANY,大于子查询结果中的某个值。
<ANY,小于子查询中的某个值。
>=ANY,大于等于子查询中的某个值。
<=ANY,小于等于子查询中的某个值。
=ANY,等于子查询中的某个值。
!=ANY或者<>ANY,不等于子查询中的某个值。
>ALL,大于子查询中的所有值。
<ALL,小于子查询中的所有值。
>=ALL,大于等于子查询中的所有值。
<=ALL,小于等于子查询中的所有值。
=ALL,等于子查询中的所有值。
!=ALL或者<>ALL,不等于子查询中的任何一个值。
6.4 HQL的多表查询
对象之间总是有各种各样的关系,关联关系是类之间最常见的关系。多表查询是HQL中的强大功能之一,包括内连接、左连接和右连接等。多表查询的设置及运行都比较麻烦,在运行本节中的示例时,务必保证每一步都没有错误。
6.4.1 表之间的关联关系
在数据库joblog中用到了3个表:student(学生表)、course(课程表)和sc(选课表)。这些表的详细信息见6.1.1节“示例中用到的默认数据库表和数据”。在现实模型中,一个学生可以选择多门课程,一个课程可以被多个学生选择,student和course是多对多的关联关系。为了便于演示HQL的多表查询,本节中假设student和course之间是单向关联关系。
在多对多的关联关系中,一般来说有个中间表,这个表描述了多对多关系,这就是选课表sc,sc每一行数据代表一个学生的选课和成绩。
各个表的主键、外键设置如下。
student表的主键是id字段。
course表的主键是id字段。
sc表的主键是id字段。
sc表中的Sno字段是student表id字段的外键。
sc表中的Cno字段是course表id字段的外键。
图6-8是3个表之间关系的直观表示。
图6-8 3个表之间的关系
在MySQL Query Browser中设置好上述关系。如果此处设置不正确,可能会影响多表连接查询。其中sc表的建表信息如下(其中包含了外键关系)。
CREATE TABLE 'joblog'. 'sc' (
'id' int(10) unsigned NOT NULL auto_increment COMMENT 'id',
'Sno' int(10) unsigned NOT NULL default '0' COMMENT '学号',
'Cno' int(10) unsigned NOT NULL default '0' COMMENT '课程号',
'Grade' int(10) unsigned default NULL COMMENT '成绩',
PRIMARY KEY ('id'),
KEY 'FK_sc_1' ('Sno'),
KEY 'FK_sc_2' ('Cno'),
CONSTRAINT 'FK_sc_1' FOREIGN KEY ('Sno') REFERENCES 'student' ('id'), /* 外键信息 */
CONSTRAINT 'FK_sc_2' FOREIGN KEY ('Cno') REFERENCES 'course' ('id') /* 外键信息 */
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;
6.4.2 表中的数据
这一节中用到了3个表的数据,student表和course表的数据如6.1节中图6-2和6-4所示,但是sc表的内容变为图6-9所示的数据,其中Sno和Cno存储的分别是student表和course表中对应的主键值。
图6-9 本节中所用的表sc中的内容
6.4.3 修改持久化类
Student对象和Course对象之间是多对多的关系。此处使用的是单向关联,仅仅建立从Student到Course的单向关联。如图6-10所示,仅有Student到Course的单向关联。
图6-10 Student到Course类的单向关联
为了建立Student到Course的单向关联关系,在Student.java中新加一个属性course。course属性是Set型的,可以在这个属性中加入多个Course对象,建立起关联关系。下面是加入course属性后的源代码,粗体部分为加入的代码。
package hibernate.ch06;
import java.util.HashSet;
import java.util.Set;
public class Student implements java.io.Serializable {
private Integer id; //ID
private Integer sno; //学号
private String sname; //姓名
private String ssex; //性别
private String sdept; //系部
private Integer sage; //年龄
private String saddress; //住址
private Set course=new HashSet(); //所选课程
public Student() {
}
//此处省略其他的构造方法
//此处省略getter/setter访问器
//course属性的get访问器
public Set getCourse() {
return course;
}
//course属性的set访问器
public void setCourse(Set course) {
this.course = course;
}
}
持久化类Course.java和SC.java无需修改。
6.4.4 在映射文件中加入关联信息
在Student.hbm.xml映射配置文件中,加入Student到Course的映射信息。关于如何映射关联关系,将在第8章讲解,读者可暂时按照下面的设置,具体含义等阅读完第八章便可理解。具体代码如下。
<set name="course" table="sc" lazy="false" cascade="save-update">
<key column="sno" />
<many-to-many class="hibernate.ch06.Course" column="cno" />
</set>
说明如下。
<set>元素是和<class>元素平行的元素。<set>元素表明将要映射的字段对应着一个集合。<set>元素包含多个属性,其中:name属性用于设置映射的持久化类的属性名称,在本例中为Student表的course属性;table属性表示多对多关联关系的中间表名称,此处为sc表;cascade表示当保存或者更新Student实例时,是否保存或更新Course对象。
<set>元素的子元素<key column="sno" />设定与student表关联的中间表sc的外键sno。
<set>元素的子元素<many-to-many>用于设定多对多关系。在该元素中,class属性用于设定多对多关系中,与Student类关联的类Course类;column属性设定中间表与course表连接的外键cno。
完整的配置文件Student.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="hibernate.ch06.Student" table="student" catalog="joblog">
<id name="id" type="integer">
<column name="id" />
<generator class="identity"></generator>
</id>
<!--映射学号-->
<property name="sno" type="integer">
<column name="Sno" not-null="true" />
</property>
<!--映射姓名-->
<property name="sname" type="string">
<column name="Sname" length="45" />
</property>
<!--映射系部-->
<property name="sdept" type="string">
<column name="Sdept" length="10" />
</property>
<!--映射年龄-->
<property name="sage" type="integer">
<column name="Sage" />
</property>
<!--映射性别-->
<property name="ssex" type="string">
<column name="Ssex" length="2" />
</property>
<!--映射住址-->
<property name="saddress" type="string">
<column name="Saddress" length="45" />
</property>
<!--联接-->
<set name="course" table="sc" lazy="false" cascade="save-update">
<key column="sno" />
<many-to-many class="hibernate.ch06.Course" column="cno" /> <!--多对多-->
</set>
</class>
</hibernate-mapping>
6.4.5 左外连接
左外连接(Left Outer Join)查询出左表对应的复合条件的所有记录,如查询李晓梅同学的选课信息。下面是类HQLLeftOuterJoinQuery的源代码。
package hibernate.ch06;
import hibernate.HibernateSessionFactory;
import java.util.Iterator;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
public class HQLLeftOuterJoinQuery {
public static void main(String[] args) {
Session session=HibernateSessionFactory.currentSession();
//HQL查询语句
String hql="from Student s left join s.course c where s.sname='李晓梅'";
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Object[] obj=(Object[])it.next();
Student stu=(Student)obj[0];
Course course=(Course)obj[1];
System.out.println("*********学生信息及其选课信息******************");
if(course!=null){
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t"+
"课程:"+course.getCname());
}else{
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t");
};
}
}
}
如果只用单表查询,只能从student表中查询出李晓梅的个人信息,而无法知道她的选课信息,因为选课信息存储在中间表sc中。HQL语句from Student s left join s.course c where s.sname='李晓梅'检索出了李晓梅的选课信息。
在HQL中使用left outer join关键字进行左外连接,outer关键字可以省略。
s.course是Student对象中的一个属性,用来存储Student对象的选课信息。在执行查询时,将根据Student.hbm.xml中的配置生成SQL语句,并检索信息。
查询的结果返回一个Object[]数组,数组的第0个元素是Student对象,第1个元素是与Object[0]中对应的学生所选课的Course对象。
HQLLeftOuterJoinQuery类在执行过程中生成的左外连接的SQL语句如下。
Hibernate:
select
student0_.id as id1_0_,
course2_.id as id4_1_,
student0_.Sno as Sno1_0_,
student0_.Sname as Sname1_0_,
student0_.Sdept as Sdept1_0_,
student0_.Sage as Sage1_0_,
student0_.Ssex as Ssex1_0_,
student0_.Saddress as Saddress1_0_,
course2_.Cno as Cno4_1_,
course2_.Cname as Cname4_1_,
course2_.Ccredit as Ccredit4_1_
from
joblog.student student0_
left outer join
sc course1_
on student0_.id=course1_.sno
left outer join
joblog.course course2_
on course1_.cno=course2_.id
where
student0_.Sname='李晓梅'
Hibernate:
select
course0_.sno as sno1_,
course0_.cno as cno1_,
course1_.id as id4_0_,
course1_.Cno as Cno4_0_,
course1_.Cname as Cname4_0_,
course1_.Ccredit as Ccredit4_0_
from
sc course0_
left outer join
joblog.course course1_
on course0_.cno=course1_.id
where
course0_.sno=?
程序的查询结果如下。
*********学生信息及其选课信息******************
20040001 李晓梅 课程:数据库
*********学生信息及其选课信息******************
20040001 李晓梅 课程:操作系统
使用如下语句将只返回Student对象。
select s from Student s left join s.course c where s.sname='李晓梅'
如下是只返回Student对象的部分代码。
Session session=HibernateSessionFactory.currentSession();
//HQL查询语句
String hql="select s from Student s left join s.course c where s.sname='李晓梅'";
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println("*********学生信息及其选课信息******************");
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t");
}
6.4.6 左外抓取连接
左外抓取连接指定在Hibernate检索数据时,采用抓取的方式,直接将数据加载到与Student对象关联的course属性中。下面是左外抓取连接的程序。
//HQL查询语句
String hql="select s from Student s left join fetch s.course c where s.sname='李晓梅'";
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println("*********学生信息及其选课信息******************");
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t");
}
左外抓取连接使用left join fetch关键字。
与左外连接不同的是:左外抓取连接query.list()返回的集合中存放Student对象的引用,与之相关联的选课信息存放在course属性中。
6.4.7 右外连接
HQL中使用关键字right outer join右外连接,outer关键字可以省略。右外连接与左外连接类似,不再赘述。
6.4.8 内连接
内连接(Inner Join)是指两个表中指定的关键字相等的值才会出现在结果集中的一种查询方式。HQL中使用关键字inner join进行内连接,下面是使用内连接的程序。
Session session=HibernateSessionFactory.currentSession() //创建Session
String hql="from Student s inner join s.course c"; //HQL查询语句
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Object[] obj=(Object[])it.next();
Student stu=(Student)obj[0];
Course course=(Course)obj[1];
System.out.println("*********学生信息及其选课信息******************");
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t"+"课程:"+course. getCname());
}
HQL中使用inner join进行内连接,内连接只关联并检索那些选了课的学生信息及其选课信息,没有选课的学生不在检索结果中。
可以使用select s from Student s inner join s.course c只返回Student对象。
6.4.9 抓取内连接
抓取内连接与内连接不同之处在于其对象的内存状态不一样。HQL中使用inner join fetch进行抓取内连接,如下程序所示。
Session session=HibernateSessionFactory.currentSession(); //创建Session
String hql="select s from Student s inner join fetch s.course c"; //HQL语句
Query query=session.createQuery(hql); //创建查询
List list=query.list(); //执行查询
Iterator it=list.iterator();
while(it.hasNext()){
Student stu=(Student)it.next();
System.out.println("*********学生信息及其选课信息******************");
System.out.println(stu.getSno()+"\t"+stu.getSname()+"\t");
}
内抓取连接使用inner join fech关键字。
它与内连接的区别是返回检索的list中存放的是Student对象的引用,与之相关联的选课信息存放在course属性中。
此章介绍的框架模式是一种非常新的开发模式。这种开发模式的功能分配是:Hibernate用来操作数据库;Spring作为开发的平台,提供MVC框架,并且提供了Hibernate和DWR的接口;DWR作为页面层的Ajax,可以极大的提高用户的体验。另外要说明的是,本章所有的代码和配置文件均来自开源程序Afuer。此程序的所有代码在光盘里,有兴趣的读者可以参阅,也可以到官方网站http://www.afuer.com上去下载最新版。下面开始介绍相关的内容。
11.1 Spring简介与Web服务规范
Spring在整个的应用中起到一个配置作用,也可以说是一个开放的平台,别的组件以此为依托运行,可以大大简化步骤。在Web应用层,Spring依然要满足相关的规范,从另一个角度看,Spring也增强了Web的可配置性。
11.1.1 Spring简介
在官方网站中,Spring也没有一个很明确的定义。它具有以下功能。
(1)可以让J2EE更容易的应用。
(2)面向接口编程,优势比面向类明显很多,并且Spring还可以让面向接口的复杂度耗费降为零(即代码冗余非常少)。
(3)提供了很多方法来配置应用。
(4)具有专业并且便捷的测试功能,能够让代码更加容易地测试。
Spring也确实在努力实现以上的功能,现在最新版为Spring 2.0。最近著名的J2EE服务器厂商BEA公司也开始兼容Spring。
11.1.2 Java Web应用服务的结构规范
在介绍Spring的配置之前,首先要介绍一下Java Web应用服务的结构规范,因为此结构规范是运行Java Web应用的基础。
在Web应用的目录下,要有一个WEB-INF的目录,这个目录中必需包含三部分内容。
(1)classes目录。用于存放和运行编译好的Java类(一般由用户自己开发)。
(2)lib目录。用于存放打包(*.jar)后的Java类(一般放置官方或者第三方做的组件)。
(3)web.xml文件。用于Web运行的基本配置。
JSP文件存放的位置,主要有两种方式。
(1)直接放在根目录下。客户端只要知道JSP目录就可以访问,但这样存放不安全。
(2)放在WEB-INF目录下,这样存放只有通过服务器的定向才可以访问,对开发有些不便。
有了以上配置,就可以配置Spring了。当然,此处的配置仅仅是对Web应用的基本配置。建议读者先去看看相关的规范
另类查询 Hibernate HQL 深度历险
传统的SQL语言采用的是结构化的查询方法,而这种方法对于查询以对象形式存在的数据却无能为力。幸运的是,Hibernate为我们提供了一种语法类似于SQL的语言,Hibernate查询语言(HQL),和SQL不同的是,HQL是一种面向对象的查询语言,它可以查询以对象形式存在的数据。因此,本文就HQL如何工作以及如何使用HQL展开了深入的讨论。
SQL本身是非常强大的。当SQL的这种强大和处理面向对象数据的能力相结合时,就产生了HQL。和SQL一样,HQL提供了丰富的查询功能,如投影查询、聚合函数、分组和约束。任何复杂的SQL都可以映射成HQL。
本文的第一部分将讨论HQL的简单用法。第二部分将讨论在HQL中如何根据上下文关系进行查询。在第三部分将以一个例子来说明如何在实际应用中使用HQL。
进入HQL世界
一个ORM框架是建立在面向对象的基础上的。最好的例子是Hibernate如何提供类SQL查询。虽然HQL的语法类似于SQL,但实际上它的查询目标是对象。HQL拥有面向对象语言的所有的特性,这其中包括多态、继承和组合。这就相当于一个面向对象的SQL,为了提供更强大的功能,HQL还提供了很多的查询函数。这些函数可以被分为四类:
1.投影函数 2.约束函数 3.聚合函数 4.分组函数
使用HQL可以建立简单的查询,也可以建立更复杂的查询。在本文中并不讨论那些非常复杂的查询,如含有子查询和很多连接的查询。本文只讨论连接两个表的查询。现在让我们开始接近HQL吧!
投影
如谓投影,就是一个可以访问的对象或对象的属性。在HQL中,可以使用from和select子句来完成这个工作。
from子句返回指定的类的所有实例。如from Order将返回Order类的所有实例。换句话说,以上的查询相当于以下的SQL语句:
select * from order
from 是最简单的查询子句。from后面可以跟一个或多个类名(类名也可以带有别名)。为了得到Order和Product的所有实例,可以使用如下的查询:
from Order, Product
和类名一样,别名也可以在from后使用,如下代码如示:
from Order as o, Product p
当查询很复杂时,加入别名可以减少语句的长度。我们可以看看如下的SQL语句:
select o.*, p.* from order o, product p where o.order_id = p.order_id
我们可以很容易看出,上面的查询是一对多的关系。在HQL中相当于一个类中包含多个其它类的实例。因此,以上的SQL写成HQL就是:
from Order as o inner join o.products as product
现在让我们考虑另外一个从表中得到指定属性的情况。这就是最常用的select子句。这在HQL中的工作方式和SQL中一样。而在HQL中,如果只是想得到类的属性的话,select语句是最后的选择。以上的SQL可以使用select子句改成如下的HQL语句:
select product from Order as o inner join o.products as product
以上的HQL语句将返回Order中的所有Products实例。如果要得到对象的某一个属性,可以将HQL语句写成如下的形式:
select product.name from Order as o inner join o.products as product
如果要得到多个对象的属性,可以将HQL语句写成如下形式:
select o.id, product.name from Order as o inner join o.products as product
接下来,我们将进入下一个议题。假设我们需要根据某些条件得到数据。那么以上所述的HQL语句将无法满足需求。为了达到这一目的,我们就要用到下面将要讨论的约束子句。
#p#
约束
从以上可知,投影返回的是所有的数据。但在大多数时候我们并不需要这么多数据。这就需要对数据进行过滤。在HQL中过滤数据的子句和SQL一样,也是where。它的语法类似于SQL,通过where子句,可以对行进行过滤。我们可以看看下面的SQL语句:
select * from orders where id = ‘1234’
这条查询语句返回了id等于1234的所有的字段。和这条SQL对等的是下面的HQL语句:
select o from Order o where o.id=’1234’
从以上两条语句可以看出,它们的where子句非常相似。而它们唯一的不同是SQL操作的是记录,而HQL操作的是对象。在HQL中,除了where子句可以过滤数据外,having子句也可以做到这一点(关于having子句的详细内容我将在分组部分讨论)。投影和约束是两个基本的操作,这两个操作再加上聚合函数的话,那HQL将变得更加强大。下面我们就来讨论什么是聚合。
聚合
上述的查询都是将每一个记录(对象)当做一个单位,而如果使用聚合,可以将一类记录(对象)当做一个单位。然后再对每一类的记录(对象)进行一系列地操作,如对某一列取平均值、求和、统计行数等等。HQL支持以下的聚合函数:
1.avg(…), sum(…) 2.min(…), max(…) 3.count(*), count(…), count(distinct…), count(all…)
以上的聚合函数都返回数值类型。这些操作都可以在select子句中使用,如下所示:
select max(o.priceTotal) + max(p.price) from Order o join o.products p group by o.id
以上的HQL语句返回了两个值的和:orders表中的priceTotal的最大值和products表中的price的最大值之和。我们还可以使用having子句对分组进行过滤。如我们想按id统计priceTotal小于1000的数量可按如下的HQL语句去实现:
select count(o) from Order o having o.priceTotal < 1000 group by o.id
我们还可以将聚合函数和having子句一起使用。如我们要按products表的id统计price小于amount的平均数的产品数量,HQL语句如下:
select count(p) from Product p having p.price < avg(amount) group by p.id
从上面的一系列的HQL语句可以看出,所有通过SQL实现的,都可以通过HQL来实现。
分组
在上一部分,已经涉及到了分组的概念。分组操作的是行的集合。它根据某一列(属性)对记录集进行分组。这一切是通过group子句实现的。如下的例子描述了group子句的一般用法。
select count(o) from Order o having o.priceTotal >= 1200 and o.priceTotal <= 3200
group by o.id
HQL中的分组和SQL中的分组类似。总之,除了一些对SQL的特殊扩展外,其它所有的SQL功能都可以使用HQL描述。在接下来的部分,让我们举例说明如何在Java中使用HQL。
在Java中使用HQL到现在为止,我们已经学习了HQL的基本用法。接下来我们举一个例子来说明如何在Java中使用HQL。下面的例子只给出了主要的部分,由于本文只是讨论HQL的用法,因此,关于Hibernate的一些设置和在main()函数中调用Hibernate的部分并未给出,读者可以参考相关的文当。现在让我们看看下面的例子。
下面是必须引用的包
import java.util.List; import org.hibernate.*; import org.hibernate.cfg.* import com.Order;
下面是类的声明
public class MyOrder { … … }
下面让我们来实现MyOrder类的构造函数
public class MyOrder { SessionFactory sf;
public MyOrder() { Configuration cfg = new Configuration().addClass(Order.class); sf = cfg.buildSessionFactory(); } … … }
下面的getOrder函数根据priceTotal的区间值返回Order对象。
public class MyOrder { …. …. public Order getOrder(String lower, String upper) { // 打开一个会话 Session sess = sf.openSession(); // HQL语句 String query = "select o from o " + "Order as o join o.products as p " + "where o.priceTotal > :priceTotalLower" + "and o.priceTotal< :priceTotalUpper";
Query q = sess.createQuery(query); // 将两个参数传入HQL中 q.setDouble("priceTotalLower", Double.parseDouble(lower)); q.setDouble("priceTotalUpper", Double.parseDouble(upper));
List list = q.list();
Order o=(Order)list.iterator.next();
return o;
} … … }
下面的main函数将测试MyOrder类
public class MyOrder { … … public static void main(String args[]) { Order o=MyOrder().getOrder(“100”, “300”); System.out.println(“id=”+ o.id); … … } }
小结
上述的代码演示了如何在Java中使用HQL,但HQL还有两点需要注意一下:
1.HQL并不区分字母的大小写,但在HQL中的Java类和属性名必须和实际的类和属性名一致。如SELECT和select之间可以互换,但Order和order却代表不同的含义。 2.如果HQL中引用的类未被导入,在HQL中必须引用具体的包。如本例中,如果com.Order未被导入,在HQL中必须将Order写成com.Order。
(责任编辑 火凤凰 sunsj@51cto.com TEL:(010)68476636-8007)