SSH Chapter 07 HQL连接查询和注解
本章目标 :
- 掌握
Hibernate
的连接查询 - 掌握聚合函数分组查询
- 掌握子查询
- 掌握注解
技术内容 :
本章将介绍HQL
的其他操作技能 , 如分组统计
, 自查询
以及HQL优化
. 最后介绍如何使用注解代替XML
文件完成对象-关系映射工作 .
1 . HQL的连接查询
基于ORM,Hibernate在对象模型和关系数据库的表之间建立了一座桥梁,通过Hibernate,程序员就不需要再使用SQL语句操纵数据库中的表,使用API直接操作JavaBean对象就可以实现数据的存储,查询,更改和删除等操作,显著降低了由于对象与关系数据库在数据表现方面的范例不匹配导致的开发成本。
1.1 各种连接查询
和SQL
查询一样,HQL
也支持多种连接查询,如内连接
,外连接
。我们知道在SQL
中可通过JOIN
子句实现多表之间的连接查询,HQL
也提供了连接查询机制,还允许显示指定迫切内连接
和迫切左外连接
。
迫切连接
是指不仅指定了连接查询方式,而且显示指定了关联级别的查询策略。迫切查询
使用fetch
关键字实现,fetch
关键字表明“左边”
对象用来与“右边”
对象关联的属性会立即被初始化。
HQL
常用的连接方式:
连接类型 | HQL语法 | 适用范围 |
---|---|---|
内连接 | inner join OR join | 适用于有关联关 |
迫切内连接 | inner join fetch OR join fetch | 系的持久化类 |
左外连接 | left outer join OR left join | 并且在映射文件 |
迫切左外连接 | left outer join fetch OR left join fetch | 中对这种关联关 |
右外连接 | right outer join OR right join | 系做了映射 |
1. 内连接
Hibernate
的内连接
语法如下:
语法:
from Entity as e [inner] join [fetch] e.property
注意:为实体类起别名
使用内连接查询部门和员工信息 , 如示例1
所示 :
示例1:
测试方法关键代码如下:
@Test
public void testInnerJoin() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
//注意使用连接语法时 需将对象的toString()方法删除掉或者注释掉
//注意查询的两张表的集合 所以返回的是object[]
// 否则程序会抛出java.lang.StackOverflowError
//普通连接以对象数组封装,关联属性未初始化
List<Object[]> list = HibernateUtil.currentSession().createQuery(
"from Dept d inner join d.emps").list();//inner 可以省略
for (Object[] objects : list) {
System.out.println(objects[0] + "\t" + objects[1]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例1, Hibernate
执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
emps1_.EMPNO as EMPNO1_1_1_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_,
emps1_.ENAME as ENAME2_1_1_,
emps1_.JOB as JOB3_1_1_,
emps1_.SAL as SAL4_1_1_,
emps1_.HiREDATE as HiREDATE5_1_1_,
emps1_.DEPTNO as DEPTNO6_1_1_
from
DEPT dept0_
inner join
EMP emps1_
on dept0_.DEPTNO=emps1_.DEPTNO
示例1
中list
集合中的每个元素都是一个Object
数组 , 数组中的第一个元素是Dept
对象 , 第二个元素是Emp
对象 , Dept
对象的emps
集合元素没有被初始化 , 即emps
集合没有存放管理的Emp
对象。
接下来使用迫切内连接查询部门和员工信息 , 如
示例2
所示。
示例2:
测试方法的关键代码如下:
@Test
public void testInnerJoinFetch() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list = HibernateUtil.currentSession().createQuery(
"from Dept d inner join fetch d.emps").list();
list.forEach(d-> System.out.println(d.getDeptName()+"\n\t"+d
.getEmps()));
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例2
,Hibernate
执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
emps1_.EMPNO as EMPNO1_1_1_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_,
emps1_.ENAME as ENAME2_1_1_,
emps1_.JOB as JOB3_1_1_,
emps1_.SAL as SAL4_1_1_,
emps1_.HiREDATE as HiREDATE5_1_1_,
emps1_.DEPTNO as DEPTNO6_1_1_,
emps1_.DEPTNO as DEPTNO6_1_0__,
emps1_.EMPNO as EMPNO1_1_0__
from
DEPT dept0_
inner join
EMP emps1_
on dept0_.DEPTNO=emps1_.DEPTNO
示例2中 , list
集合中的每个元素都是Dept
对象 , Hibernate
使用fetch
关键字实现了将Emp
对象读取出来后立即填充到对应的Dept
对象的集合属性中。
通过观察
示例1
和示例2
在控制台的输出 , 发现会有一些重复的Dept
对象 , 可以使用select distinct d from Dept d inner join fetch d.emps
解决这一问题 , 如示例3
所示:
示例3:
测试方法关键代码如下:
@Test
public void testInnerJoinFetch_Distinct() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list = HibernateUtil.currentSession().createQuery(
"select distinct d from " +
"Dept d inner join fetch " +
"d.emps").list();
list.forEach(d -> System.out.println(d.getDeptName() + "\n\t" + d.getEmps()));
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例3
, 控制台输出的部门信息没有重复!
2. 外连接
(1) Hibernate
的左外连接
语法如下:
语法 :
from Entity as e left [outer] join [fetch] e.property
注意:为实体类起别名
fetch
关键字用来指定查询策略 .
修改示例3中HQL
语句 , 将内连接HQL
语句修改为左外连接的HQL
语句 : from Dept d left join fetch d.emps
, 详细代码如下:
/**
* 使用迫切左外连接查询部门和员工的信息。
*/
@Test
public void testLeftJoinFetch() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list = HibernateUtil.currentSession().createQuery(
"from Dept d left join fetch " +
"d.emps").list();
list.forEach(d -> System.out.println(d.getDeptName() + "\n\t" + d
.getEmps()));
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
执行测试方法 , Hibernate
执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
emps1_.EMPNO as EMPNO1_1_1_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_,
emps1_.ENAME as ENAME2_1_1_,
emps1_.JOB as JOB3_1_1_,
emps1_.SAL as SAL4_1_1_,
emps1_.HiREDATE as HiREDATE5_1_1_,
emps1_.DEPTNO as DEPTNO6_1_1_,
emps1_.DEPTNO as DEPTNO6_1_0__,
emps1_.EMPNO as EMPNO1_1_0__
from
DEPT dept0_
left outer join
EMP emps1_
on dept0_.DEPTNO=emps1_.DEPTNO
Hibernate
使用fetch
关键字实现了将Emp
对象读出来后立即填充到对应的Dept
对象的集合属性中 .
(2) Hibernate
的右外连接语法如下:
语法:
from Entity as e right [outer] join e.property
注意:为实体类起别名
注意:
fetch
关键字只对 inner join
和 left join
有效 .
对于
right outer join
而言 , 由于作为关系容器的左边
对象可能为null
, 所以也就无法通过fetch
关键字强制Hibernate
进行集合填充操作.
在实际开发中很少使用右外连接 .
3. 等值连接
HQL
支持SQL
风格的等值连接
。等值连接
适用于**两个类之间没有定义任何关联时,如统计报表数据。**在where
子句中,通过属性作为筛选条件,语法如下:
from Dept d , Emp e where d=e.dept
示例4:
演示等值连接 , 增加测试方法 , 代码如下:
/**
* 使用等值连接查询部门和员工的信息。
*/
@Test
public void testCross() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Object[]> list = HibernateUtil.currentSession().createQuery(
"from Dept as d,Emp as e " +
"where d=e.dept").list();//注意类型必须匹配
for (Object[] obj : list) {
System.out.println(obj[0] + "\t" + obj[1]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例4
测试的方法 , Hibernate
执行以下select
语句:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_0_,
emp1_.EMPNO as EMPNO1_1_1_,
dept0_.DNAME as DNAME2_0_0_,
dept0_.LOC as LOC3_0_0_,
emp1_.ENAME as ENAME2_1_1_,
emp1_.JOB as JOB3_1_1_,
emp1_.SAL as SAL4_1_1_,
emp1_.HiREDATE as HiREDATE5_1_1_,
emp1_.DEPTNO as DEPTNO6_1_1_
from
DEPT dept0_ cross
join
EMP emp1_
where
dept0_.DEPTNO=emp1_.DEPTNO
使用等值连接时应避免"from Dept , Emp"
这样的语句出现 . 执行这条HQL
查询语句 , 返回DEPT
表和EMP
表交叉组合 , 结果集的记录数为两个表的记录数之积 , 也就是数据库中的笛卡尔积
. 这样的查询结果没有意义 .
4. 隐式内连接
在HQL
查询语句中,对Emp
类可以通过dept.deptName
的形式访问其关联的dept
对象的deptName
属性。
使用隐式内连接按部门条件查询员工信息 , 如
示例5
所示:
示例5:
测试方法中的关键代码如下:
@Test
public void testImplInnerJoin() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Emp> list = HibernateUtil.currentSession().createQuery(
"from Emp e where e.dept.deptName=:deptName").setParameter("deptName","SALES").list();
list.forEach(System.out::println);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例5
, Hibernate
执行以下select
语句:
Hibernate:
select
emp0_.EMPNO as EMPNO1_1_,
emp0_.ENAME as ENAME2_1_,
emp0_.JOB as JOB3_1_,
emp0_.SAL as SAL4_1_,
emp0_.HiREDATE as HiREDATE5_1_,
emp0_.DEPTNO as DEPTNO6_1_
from
EMP emp0_ cross
join
DEPT dept1_
where
emp0_.DEPTNO=dept1_.DEPTNO
and dept1_.DNAME=?
示例5中的HQL
语句未使用任何连接的语法 , 而Hibernate
会根据类型之间的关联关系 , 自动使用等值连接(等效内连接)的方式实现查询 , 这是所谓的隐式内连接 .
隐式内连接使得编写
HQL
语句时能够以更加面向对象的方式进行思考,更多地依据对象之间的关系,而不必过多考虑数据库表的结构。
隐式内连接也可以用在select
子句中 , 例如:
select e.empName , e.dept.deptName from Emp e
2 . 分组统计数据
HQL
和SQL
语句一样,使用GROUP BY
关键字对数据分组,使用HAVING
关键字对分组数据设定约束条件,从而对数据完成分组和统计。基本语法如下:
语法:
[select xxx] from xxx [where xxx] [group by xxx [having xxx] ] [order by xxx]
2.1 聚合函数
聚合函数:常被用来实现数据统计功能 , HQL
查询语句中常用的聚合函数如下:
函数名称 | 说明 |
---|---|
count() | 统计记录条数 |
sum() | 求和 |
max() | 求最大值 |
min() | 求最小值 |
avg() | 求平均值 |
(1) count( )
: 统计记录数
例如 : 查询DEPT
表中所有记录的条数:
测试方法关键代码如下:
/**
* 查询DEPT表中所有记录条数
*/
@Test
public void testDeptCount() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
Long count = HibernateUtil.currentSession().createQuery(
"select count(id) from Dept", Long.class).uniqueResult();
System.out.println(count);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
执行测试方法 , Hibernate
执行以下SQL
语句:
Hibernate:
select
count(dept0_.DEPTNO) as col_0_0_
from
DEPT dept0_
(2) sum( )
: 求和
例如 : 计算所有员工应发的工资总和 :
测试方法代码如下:
/**
* 计算EMP表中所有员工应发的工资总和
*/
@Test
public void testTotalSalary() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
double sum = HibernateUtil.currentSession().createQuery(
"select sum(salary) from Emp", Double.class).uniqueResult();
System.out.println(sum);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法 , Hibernate
执行以下SQL
语句:
Hibernate:
select
sum(emp0_.SAL) as col_0_0_
from
EMP emp0_
(3) min( )
: 求最小值
例如 : 计算员工的最低工资是多少
测试方法关键代码如下:
/**
* 计算EMP表中员工的最低工资是多少
*/
@Test
public void testMinSalary() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
double min = HibernateUtil.currentSession().createQuery(
"select min(salary) from Emp", Double.class).uniqueResult();
System.out.println(min);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法 , Hibernate
执行以下SQL
语句:
Hibernate:
select
min(emp0_.SAL) as col_0_0_
from
EMP emp0_
(4) max( )
: 求最大值
例如 : 员工的最高工资是多少
测试方法关键代码如下:
/**
* 计算EMP表中员工的最高工资是多少
*/
@Test
public void testMaxSalary() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
double max = HibernateUtil.currentSession().createQuery(
"select max(salary) from Emp", Double.class).uniqueResult();
System.out.println(max);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法 , Hibernate
执行以下SQL
语句:
Hibernate:
select
max(emp0_.SAL) as col_0_0_
from
EMP emp0_
(5) avg()
: 求平均值 :
例如 : 员工的平均工资是多少
测试方法关键代码如下:
/**
* 计算EMP表中员工的平均工资是多少
*/
@Test
public void testAvgSalary() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
double avg = HibernateUtil.currentSession().createQuery(
"select avg(salary) from Emp", Double.class).uniqueResult();
System.out.println(avg);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法 , Hibernate
执行以下SQL
语句:
Hibernate:
select
avg(emp0_.SAL) as col_0_0_
from
EMP emp0_
小结 :
HQL查询语句很灵活,它提供了SQL常用的几乎所有功能,可以利用select子句同时查询出多个聚合函数的结果。例如 , 查询出员工最低工资,最高工资以及平均工资:
测试方法关键代码如下:
/**
* 计算EMP表中最低工资,最高工资,以及平均工资
*/
@Test
public void testSalary() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
Object[] salarys =
(Object[]) HibernateUtil.currentSession().createQuery(
"select min(salary),max(salary) ,avg(salary) from Emp").uniqueResult();
System.out.println(salarys[0] + "," + salarys[1] + "," + salarys[2]);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
因为选取多个对象 , 所以uniqueResult()
方法返回的是一个Object
数组 .
经验 :
HQL
查询语句可以返回各种类型的查询结果 . 调试程序时 , 当不能确定查询结果的类型时 , 可以使用以下方法来确定查询结果类型 。
例如 : 统计职位记录 , 判断统计结果的数据类型。
/**
* 统计职位记录 , 判断统计结果的数据类型
*/
@Test
public void testGetType() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
Object count = HibernateUtil.currentSession().createQuery(
"select count(distinct job) from Emp").uniqueResult();
System.out.println(count.getClass().getName());
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
count.getClass().getName()
显示查询结果类型为:java.lang.Long
.
2.2 分组查询
HQL
查询语句使用group by
子句进行分组查询 . 使用having
字句筛选分组结果 . 下面通过示例
说明 .
(1) 按职位统计员工个数
如
示例6
所示:
示例6:
测试方法关键代码如下:
@Test
public void testEg_1() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Object[]> list =
HibernateUtil.currentSession().createQuery(
"select e.job,count(e) from Emp e group by e.job").list();
for (Object[] obj : list) {
System.out.println(obj[0] + "," + obj[1]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例6 , Hibernate
执行以下select
语句:
Hibernate:
select
emp0_.JOB as col_0_0_,
count(emp0_.EMPNO) as col_1_0_
from
EMP emp0_
group by
emp0_.JOB
Query
的list()
方法返回集合中包含Object[]
类型的元素 , 每个Object[]
对应查询结果中的一条记录 , 数组第一个元素是职位名称 , 第二个元素是该职位的员工个数.
(2). 统计各部门的平均工资
如示例7
所示:
示例7:
测试方法中的关键代码如下:
/**
* 统计各部门的平均工资。
*/
@Test
public void testEg_2() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Object[]> list =
HibernateUtil.currentSession().createQuery(
"select e.dept.deptName,avg(e.salary) from Emp e " +
"group by e.dept.deptName").list();
for (Object[] obj : list) {
System.out.println(obj[0] + "," + obj[1]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例7
时,Hibernate
会执行以下select
语句:
Hibernate:
select
dept1_.DNAME as col_0_0_,
avg(emp0_.SAL) as col_1_0_
from
EMP emp0_ cross
join
DEPT dept1_
where
emp0_.DEPTNO=dept1_.DEPTNO
group by
dept1_.DNAME
Query
的list()
方法返回集合中包含Object[]
类型的元素 , 每个Object[]
对应查询结果中的一条记录 , 数组第一个元素是部门名称 , 第二个元素是该部门员工的平均工资.
(3) 统计各个职位的最低工资和最高工资 .
如示例8
所示:
示例8:
测试方法中的关键代码如下:
/**
* 统计各个职位的最低工资和最高工资
*/
@Test
public void testEg_3() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Object[]> list =
HibernateUtil.currentSession().createQuery(
"select e.job,min(e.salary),max(e.salary) from " +
"Emp e group by e.job").list();
for (Object[] obj : list) {
System.out.println(obj[0] + "," + obj[1] +","+obj[2]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例8,Hibernate
执行以下查询语句:
Hibernate:
select
emp0_.JOB as col_0_0_,
min(emp0_.SAL) as col_1_0_,
max(emp0_.SAL) as col_2_0_
from
EMP emp0_
group by
emp0_.JOB
Query
的list()
方法返回集合中包含Object[]
类型的元素 , 每个Object[]
对应查询结果中的一条记录 , 数组第一个元素是职位名称 , 第二个元素是该职位员工的最低工资 , 第三个元素是该职位员工的最高工资.
(4) 统计各个部门平均工资高于4000的部门名称
输出部门名称 , 部门的平均工资 . 如示例9
所示:
示例9:
测试方法关键代码如下:
/**
* 统计各个部门平均工资高于4000的部门名称 , 输出部门名称 , 部门的平均工资
*/
@Test
public void testEg_4() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Object[]> list =
HibernateUtil.currentSession().createQuery(
"select e.dept.deptName,avg(e.salary) from " +
"Emp e group by e.dept.deptName " +
" having avg(e.salary) > 4000 ").list();
for (Object[] obj : list) {
System.out.println(obj[0] + "," + obj[1]);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例9,Hibernate
执行以下查询语句:
Hibernate:
select
dept1_.DNAME as col_0_0_,
avg(emp0_.SAL) as col_1_0_
from
EMP emp0_ cross
join
DEPT dept1_
where
emp0_.DEPTNO=dept1_.DEPTNO
group by
dept1_.DNAME
having
avg(emp0_.SAL)>4000
having
字句用于筛选分组结果 . Query
的list()
方法返回集合中包含Object[]
类型的元素 , 每个Object[]
对应查询结果中的一条记录 , 数组第一个元素是部门名称 , 第二个元素是该部门员工的平均工资 .
经验:
使用
select
字句时 ,Hibernate
返回的查询结果为关系数据而不是持久化对象 , 不会占用Session
缓存 . 为了方便访问 , 可以定义一个JavaBean
来封装查询结果中的关系数据 , 使应用程序可以按照面向对象的方式来访问查询结果 . 代码示例10
所示:
示例10:
定义普通的JavaBean
, 用来封装查询结果中的关系数据,代码如下:
/**
* 封装查询数据
*/
public class DeptSalary {
private String deptName;
private Double avgSalary;
public DeptSalary() {
}
public DeptSalary(String deptName, Double avgSalary) {
this.deptName = deptName;
this.avgSalary = avgSalary;
}
//省略getter setter toString
}
测试方法代码如下:
/**
* 统计各个部门平均工资高于2000元的部门名称,打印部门名称、部门平均工资,使用JavaBean封装查询结果
*/
@Test
public void testEg_5() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<DeptSalary> list =
HibernateUtil.currentSession().createQuery(
"select new cn.hibernatedemo.entity.DeptSalary(" +
"e.dept.deptName,avg(e.salary))" +
"from " +
"Emp e group by e.dept.deptName " +
" having avg(e.salary) > 2000 ").list();
for (DeptSalary deptSalary: list) {
System.out.println(deptSalary);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法 , Hibernate
执行的SQL
语句如下:
Hibernate:
select
dept1_.DNAME as col_0_0_,
avg(emp0_.SAL) as col_1_0_
from
EMP emp0_ cross
join
DEPT dept1_
where
emp0_.DEPTNO=dept1_.DEPTNO
group by
dept1_.DNAME
having
avg(emp0_.SAL)>2000
控制台打印的结果如下:
DeptSalary{deptName='ACCOUNTING', avgSalary=2916.6666666666665}
DeptSalary{deptName='RESEARCH', avgSalary=2518.75}
3 . 子查询
子查询应用在HQL
语句的where
字句中 , 子查询语句需放在()
里面 .
例如 , 查询工资高于平均工资的员工 , 代码如下
from Emp e where e.salary > (select avg(salary) from Emp)
3.1 子查询关键字
如果子查询语句返回多条记录 , 可以使用下标的关键字进行量化:
关键字 | 说明 |
---|---|
all | 返回的所有记录 |
any | 返回的任意一条记录 |
some | 和“any” 意思相同 |
in | 与“=any” 意思相同 |
exists | 至少返回一条记录 |
下面演示使用表中的关键字进行查询 .
(1) 查询所有员工工资都小于5000元的部门
如示例11
所示:
示例11:
测试方法关键代码如下:
/**
* 查询所有工资都小于5000元的部门
*/
@Test
public void testEg_6() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
//注意此处的hql语句 必须将条件写在之前 all写在之后 必须使用该类关联的属性
//若单独使用Emp 则不会有连接条件 所以必须使用 dept的emps属性作为子查询的对象
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where 5000 > all(select e.salary " +
"from d.emps e )").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例11
,得到与以下类似的查询结果:
Dept{deptNo=20, deptName='RESEARCH', location='DALLAS'}
Dept{deptNo=30, deptName='SALES', location='CHICAGO'}
Dept{deptNo=40, deptName='OPERATIONS', location='BOSTON'}
在示例11
中 , 可以发现没有员工的40
号部门也会被查询出来 , 若不想查询没有员工的部门 , 可以使用下面的size
属性进行处理:
修改测试方法 , 关键代码如下:
/**
* 查询所有工资都小于5000元的部门
*/
@Test
public void testEg_7() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where 5000 > all(select e.salary " +
"from d.emps e ) and d.emps.size > 0").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行测试方法,查询结果如下:
Dept{deptNo=20, deptName='RESEARCH', location='DALLAS'}
Dept{deptNo=30, deptName='SALES', location='CHICAGO'}
(2) 查询至少有一位员工的工资低于5000
元的部门
如示例12
所示:
示例12:
测试方法中的关键代码如下:
/**
* 查询至少有一位员工的工资低于5000元的部门
*/
@Test
public void testEg_8() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where 5000 > any(select e.salary " +
"from d.emps e )").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例12
, 得到以下类似的查询结果:
Dept{deptNo=10, deptName='ACCOUNTING', location='NEW YORK'}
Dept{deptNo=20, deptName='RESEARCH', location='DALLAS'}
Dept{deptNo=30, deptName='SALES', location='CHICAGO'}
ACCOUNTING
,RESEARCH
,SALES
都有工资低于5000元的员工.
(3) 查询有员工工资正好是5000
元的部门
如示例13
所示:
示例13:
测试方法中的关键代码如下:
/**
* 查询有员工工资正好是5000元的部门
*/
@Test
public void testEg_9() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where 5000 = any(select e.salary " +
"from d.emps e )").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例13
, 得到与以下类似的查询结果:
Dept{deptNo=10, deptName='ACCOUNTING', location='NEW YORK'}
ACCOUNTING
有一位员工的工资正好是5000
元 . 这条HQL
语句也可以采用以下两种形式:
from Dept d where 5000=some(select e.salary from d.emps e)
或者
from Dept d where 5000 in (select e.salary from d.emps e)
(4) 查询至少有一位员工的部门
如示例14
所示:
示例14:
测试方法中关键代码如下:
/**
* 查询至少有一位员工的部门
*/
@Test
public void testEg_10() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where exists(from d.emps)").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例14
, 得到以下类似的查询结果:
Dept{deptNo=10, deptName='ACCOUNTING', location='NEW YORK'}
Dept{deptNo=20, deptName='RESEARCH', location='DALLAS'}
Dept{deptNo=30, deptName='SALES', location='CHICAGO'}
ACCOUNTING
,RESEARCH
,SALES
至少有一位员工 .
3.2 操作集合的函数或属性
HQL
提供了操作结合的函数或属性 , 如下表所示:
函数或属性 | 说 明 |
---|---|
size() 或 size | 获取集合中元素的数目 |
minIndex() 或 minIndex | 对于建立了索引的集合,获得最小的索引 |
maxIndex() 或 maxIndex | 对于建立了索引的集合,获得最大的索引 |
minElement() 或 minElement | 对于包含基本类型元素的集合,获取最小值元素 |
maxElement() 或 maxElement | 对于包含基本类型元素的集合,获取最大值元素 |
elements() | 获取集合中的所有元素 |
下面使用部分函数进行查询
(1). 查询指定员工所在的部门
如示例15
所示 :
示例15:
测试方法关键代码如下:
/**
* 查询指定员工所在的部门
*/
@Test
public void testEg_11() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
//构建查询条件
Emp emp = new Emp();
emp.setEmpNo(7934);
//注意:
//方式一:from Dept d where :emp in (from d.emps)
//方式二:如果使用elements 则必须不能使用from
//直接写该类关联的那个属性即可
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where :emp in elements(d.emps)").setParameter("emp",emp).list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
运行示例15
, 得到以下类似的查询结果:
Dept{deptNo=10, deptName='ACCOUNTING', location='NEW YORK'}
编号是7934
的员工 在10
号部门 , 此员工对应的OID
是7934
. 这条HQL
查询语句也可以采用以下形式:
from Dept d where :emp in (from d.emps)
(2). 查询员工个数大于5的部门
如示例16
所示:
示例16:
测试方法中的关键代码 :
/**
* 查询指定员工个数大于5的部门
*/
@Test
public void testEg_13() {
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
//也可以使用:from Dept d where size(d.emps) > 5
List<Dept> list =
HibernateUtil.currentSession().createQuery(
"from Dept d where d.emps.size>5").list();
for (Dept dept : list) {
System.out.println(dept);
}
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
这条HQL
查询语句也可以采用以下形式:
from Dept d where size(d.emps) > 5
以上HQL
查询语句对应的SQL
语句中包含子查询
:
Hibernate:
select
dept0_.DEPTNO as DEPTNO1_0_,
dept0_.DNAME as DNAME2_0_,
dept0_.LOC as LOC3_0_
from
DEPT dept0_
where
(
select
count(emps1_.DEPTNO)
from
EMP emps1_
where
dept0_.DEPTNO=emps1_.DEPTNO
)>5
4 . 查询性能优化
1. Hibernate
查询优化策略
(1) 使用迫切左外连接或迫切内连接查询策略,配置二级缓存和查询等方式 , 减少select
语句的数目 , 降低访问数据库的频率 .
(2) 使用延迟加载等方式避免加载多余的不需要访问的数据 .
(3) 使用Query
接口的iterate()
方法减少select
语句中的字段 , 降低访问数据库的数据量 , 并结合缓存等机制减少数据库访问次数 , 提高查询效率
经验:
list()
方法:
无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存
Hibernate是通过一条Select SQL获取所有的记录。并将其读出,填入到POJO中返回.所以list()方法返回的每个对象都是完整的(对象中的每个属性都被表中的字段填充上了)。所以返回的每个对象都是原本的对象。
iterate()
方法:
iterate
()方法所返回的对象中仅包含了主键值(标识符),只有当你对iterate
中的对象进行操作时,Hibernate
才会向数据库再次发送SQL
语句来获取该对象的属性值。所以返回的对象是代理对象。list
方法需要1条SQL语句,iterate
可能需要N+1条。
可以充分利用缓存,可以对缓存读和写。返回对象的主键值,hibernate
首先会根据这个id 在本地Cache 内寻找对应的数据,如果没找到,再去数据库中检索。如果目标数据只读或者读取频繁,使用iterate()
方法可以减少性能开销。
2. HQL
优化
HQL
优化是Hibernate
程序性能优化的一个方面,HQL
和SQL
的语法非常类似。HQL
是基于SQL
的,只是增加了面向对象的封装。如果抛开HQL
同Hibernate
本身一些缓存机制的关联,HQL
的优化技巧同SQL
的优化技巧一样。在编写HQL
时,需要注意以下几个原则:
(1) 避免or
操作的使用不当
如果where
子句中有多个条件,并且其中某个条件没有索引,使用or
将导致全表扫描。假定在HOUSE
表中TITL
E有索引,PRICE
没有索引,执行以下HQL
语句:
from House where title='出租一居室' or price < 1500
当price比较时,会引起全表扫描。
(2) 避免使用not
如果where
条件的子句包含not
关键字,那么执行时该字段的索引失效。这些语句需要分成不同情况区别对待,如查询租金不多于1800
元的租房信息的HQL
语句:
from House as h where not (h.price>1800)
对于这种不大于、不小于的条件,建议使用比较运算符代替not
,如下不大于就是小于等于。所以上述查询的HQL
语句也可以为:
from House as h where h.price <= 1800
(3) 避免like
的特殊形式
如果like
以一个"%"
或"_"
开始即前模糊,则该字段的索引不起作用。但是非常遗憾的是,对于这种问题并没有额外的解决方法,只能通过改变索引字段的形式变相的解决。
(4) 避免having
子句
应尽可能的在where
子句而不是having
子句中指定条件。having
是在检索出所有记录后才对结果集进行过滤,这个处理需要一定的开销,而where
子句限制记录的数目,能减少这方面的开销
(5) 避免使用distinct
指定distinct
会导致在结果中删除重复的行。这会对处理时间造成一定的影响,因此在不要求或允许冗余时,应避免使用distinct
。
(6) 索引在以下情况下失效,应注意使用
- 只要对字段使用函数,该字段的索引不起作用,如
substring(aa,1,2)='xxx'
- 只要对字段进行计算,该字段的索引不起作用,如
price+10
5 . 注解
5.1 简介
在Hibernate
中使用注解,主要是为了替代映射文件,完成“类到表,属性到字段”的映射。
JPA
提供了一套功能强大的注解。Hibernate
直接使用了JPA
的这套注解。
当然,对于JPA
中的一些不足,Hibernate
又开发了一些自己的注解。这些注解要么继承自JPA
,要么是独立的注解,作为JPA
的补充。
故,我们使用的注解,基本是javax.persistence
包中的。
使用注解时需注意以下几点:
- 使用的均为
javax.persistence
包中的注解 - 不再需要映射文件了
- 在
Hibernate
主配置文件中无需指定映射文件了,但需要指定注解的实体类 , 语法如下:。(springboot
这一步也省了)
<mapping class="持久化类的完全限定名" />
JPA
全称Java Persistence API
, 它通过JDK 5.0
注解或XML
描述对象和关系表的映射关系 , 并将运行期对象持久化到数据库 . Hibernate
提供了JPA
的实现.
5.2 使用注解配置持久化类
注 解 | 含义和作用 |
---|---|
@Entity | 将一个类声明为一个持久化类 |
@Table | 为持久化类映射指定表 |
@Id | 声明了持久化类的标识属性 |
@GeneratedValue | 定义标识属性值的生成策略 |
@SequenceGenerator | 定义序列生产器 |
@Column | 将属性映射到列(字段) |
@Transient | 将忽略这些属性 |
下面举例说明使用注解映射员工信息 , 如示例17
所示 :
示例17 :
持久化类Emp
中的关键代码如下:
/**
* 雇员类
*/
@Entity//将一个类声明为一个持久化类,默认所有的属性都会映射到数据库成同名字段,可使用注解忽略
@Table(name="EMP")//为持久化类映射指定表
public class Emp implements Serializable {
@Id//声明了持久化类的标识属性
@GeneratedValue(strategy=GenerationType.SEQUENCE,generator = "seq_emp")//定义标识属性值的生成策略
//注意序列必须定义起始值 以及 自增值
@SequenceGenerator(name="seq_emp",sequenceName = "seq_emp_id",
allocationSize = 10,initialValue = 1)//定义序列生产器
private Integer empNo;
@Column(name="ENAME")
private String empName;
private String job;
@Column(name="SAL")
private Double salary;
private Date hireDate;
@Transient
private Dept dept;
//省略getter setter toString
}
hibernate.cfg.xml
文件中的关键代码如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.url">jdbc:oracle:thin:@localhost:1521:orcl
</property>
<property name="connection.username">scott</property>
<property name="connection.password">tiger</property>
<property name="connection.driver_class">
oracle.jdbc.driver.OracleDriver
</property>
<property name="dialect">
org.hibernate.dialect.Oracle10gDialect
</property>
<property name="current_session_context_class">thread</property>
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<!--<mapping resource="cn/hibernatedemo/entity/Dept.hbm.xml"/>-->
<!--<mapping resource="cn/hibernatedemo/entity/Emp.hbm.xml"/>-->
<mapping class="cn.hibernatedemo.entity.Emp"/>
<!-- DB schema will be updated if needed -->
<!-- <property name="hbm2ddl.auto">update</property> -->
</session-factory>
</hibernate-configuration>
示例17
将Emp
类映射到EMP
表 , empNo
属性为OID
, 主键采用序列
生成 , 需在数据库中创建名为seq_emp_id
的序列 , 语法如下:
create sequence seq_emp_id
start with 1
increment by 10;
注解可以配置在属性定义的上方 , 或者getter
方法的上方
下面对示例17
中部分注解做进一步介绍 :
@Table
: 可以省略 , 省略时默认表名与持久化类名相同@GeneratedValue
: 指定了OID
的生成策略 , 不使用此注解时 , 默认OID
由程序赋值(相当于在映射问文件中指定assigned
) . 其属性strategy
用于指定主键的生成策略。其值为系统定义好的四种策略之一。默认为AUTO
。JPA提供了四种标准用法:GenerationType.AUTO
:根据底层数据库自动选择(默认)若数据库支持自增长,则为自增长。类似于配置文件中的native
生成策略。GenerationType.IDENTITY
:根据数据库的Identity
字段生成。类似于配置文件中的identity
生成策略。GenerationType.TABLE
:使用指定表来决定主键取值GenerationType.SEQUENCE
:使用Sequence
来决定主键的取值。类似于配置文件中的Sequence
生成策略。
@SequenceGenerator
: 设置了序列生成器 ,name="seq_emp"
定义序列生成器的名称为seq_name
;sequenceName="seq_emp_id"
指定了序列的名称为seq_emp_id
;initialValue
设置了主键起始值;allocationSize
设置了生成器分配id
时的增量 ;
@Column
: 用于指定属性映射的数据库字段名 , 若不指定 , 默认字段名和属性相同@Transient
: 用于忽略不需要持久化到数据库中的属性 .dept
作为关联属性 , 其映射方式将在下面中介绍 , 这里也暂时忽略该属性
注意 : 注意使用注解定义持久化类 , 在hibernate.cfg.xml
的mapping
元素中要使用class
属性进行声明 , 指定该持久化类的全类名
在示例17
的映射基础上 , 完成对Emp
对象的添加操作 , 如示例18
所示:
示例18 :
EmpDao.java
中的关键代码如下:
public class EmpDao {
public void save(Emp emp){
HibernateUtil.currentSession().save(emp);
}
}
EmpBiz.java
中的关键代码如下:
public class EmpBiz {
private EmpDao empDao = new EmpDao();
public void addNewEmp(Emp emp){
Transaction transaction = null;
try {
transaction = HibernateUtil.currentSession().beginTransaction();
empDao.save(emp);
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
HibernateUtil.rollback(transaction);
}
}
}
测试方法的关键代码如下:
@Test
public void testAddNewEmp(){
Emp emp = new Emp();
emp.setEmpName("张三");
emp.setHireDate(new Date());
EmpBiz empBiz = new EmpBiz();
empBiz.addNewEmp(emp);
}
5.3 使用注解配置关联关系
通过下表所示的注解可以完成对象关联关系的常用配置 .
注 解 | 含义和作用 |
---|---|
@OneToOne | 建立持久化类之间的一对一关联关系 |
@OneToMany | 建立持久化类之间的一对多关联关系 |
@ManyToOne | 建立持久化类之间的多对一关联关系 |
@JoinColumn | 和@ManyToOne配合,指定外键列 |
@ManyToMany | 建立持久化类之间的多对多关联关系 |
对象关联关系中使用频率最高的是一对多(多对一)
关联关系 . 下面以此为例讲解注解的配置 , 如示例19
所示 . 其他关联关系的注解配置与此类似 .
示例19:
持久化类Emp
中的关键代码如下:
/**
* 雇员类
*/
@Entity//将一个类声明为一个持久化类,默认所有的属性都会映射到数据库成同名字段,可使用注解忽略
@Table(name="EMP")//为持久化类映射指定表
public class Emp implements Serializable {
@Id//声明了持久化类的标识属性
@GeneratedValue(strategy = GenerationType.SEQUENCE,generator="seq_emp")//定义标识属性值的生成策略
@SequenceGenerator(name="seq_emp",sequenceName = "seq_emp_id",
allocationSize = 10,initialValue = 1)//定义序列生产器
private Integer empNo;
@Column(name="ENAME")
private String empName;
private String job;
@Column(name="SAL")
private Double salary;
private Date hireDate;
//@Transient
@ManyToOne(fetch = FetchType.LAZY)
//name:当前表的字段
//referencedColumnName:引用表对应的字段,如果不注明,默认就是引用表的主键
@JoinColumn(name = "DEPTNO",referencedColumnName = "DEPTNO")
private Dept dept;
//省略getter setter 以及toString
}
持久化类Dept
中的关键代码如下:
/**
* 部门信息
*/
@Entity
@Table(name = "DEPT")
public class Dept implements Serializable {
//private Byte deptNo;
@Id
private Short deptNo;
@Column(name="DNAME")
private String deptName;
@Column(name="LOC")
private String location;
/**
* 因为mappedBy是定义在Dept中,即Dept类不负责维护级联关系.即维护者是Emp.
* 所以:
* 1.要将Dept的数据,赋给Emp,即用emp的setDept()方法去捆定Dept数据;
* 2.在进行数据插入/更新session.save()/session.update()时,
* 最后操作的是Emp,外键关系也是由Emp维护
*/
//private Set<Emp> emps;//部门对应多个员工,即一对多的关系
@OneToMany(mappedBy = "dept",cascade = CascadeType.ALL)
private Set<Emp> emps = new HashSet<>();
//方便赋值,这里可以直接创建实例化
//省略getter setter toString
}
hibernate.cfg.xml
中的关键代码如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.url">jdbc:oracle:thin:@localhost:1521:orcl
</property>
<property name="connection.username">scott</property>
<property name="connection.password">tiger</property>
<property name="connection.driver_class">
oracle.jdbc.driver.OracleDriver
</property>
<property name="dialect">
org.hibernate.dialect.Oracle10gDialect
</property>
<property name="current_session_context_class">thread</property>
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<!--<mapping resource="cn/hibernatedemo/entity/Dept.hbm.xml"/>-->
<!--<mapping resource="cn/hibernatedemo/entity/Emp.hbm.xml"/>-->
<mapping class="cn.hibernatedemo.entity.Emp"/>
<mapping class="cn.hibernatedemo.entity.Dept"/>
<!-- DB schema will be updated if needed -->
<!-- <property name="hbm2ddl.auto">update</property> -->
</session-factory>
</hibernate-configuration>
通过fetch
属性指定加载方式,有两个值:
- FetchType.LAZY:延迟加载
- FetchType.EAGER:急加载
示例19
中使用@ManyToOne
注解配置Emp
类和Dept
类之间的多对一
关联 . 其属性fetch=FetchType.LAZY
设置关联级别采用延迟策略 ; 若不指定,则该属性的默认值是EAGER
, 查询Emp
时 ,Hibernate
将使用左外连接将相关的Dept
对象一并查出。
注解@JoinColumn(name="DEPTNO")
指定了维系关系的外键字段是EMP
表的DEPTNO
注解
@OneToMany
配置了Dept
类和Emp
类之间的一对多
的关系 . 属性mappedBy="dept"
将关联关系的控制权交给Emp
类的这一方 , 相当于Dept.hbm.xml
中配置的inverse="true"
.mappedBy
属性的值是Emp
类中与Dept
类关联的属性名 . 属性cascade = CascadeType.ALL
设置了级联操作类型 ,cascade
属性可选的取值如下:
- CascadeType.PERSIST 级联新增(又称级联保存)
- CascadeType.MERGE 级联合并(又称级联更新)
- CascadeType.REMOVE 级联删除
- CascadeType.REFRESH 级联刷新
- CascadeType.ALL 包含所有持久化方法
以上取值在使用中可以多选 , 多选时用逗号隔开
注意:用主动维护关系的一方来插入数据,不仅可以进行级联操作,也能保证外键关系的正常
基于示例19
的配置 . 下面尝试添加部门 , 同时添加员工 ,修改示例19
的测试方法, 如示例20
所示:
示例20 :
测试方法中的关键代码如下 :
@Test
public void testAddNewEmp(){
Emp emp = new Emp();
emp.setEmpName("李四");
emp.setHireDate(new Date());
Dept dept = new Dept();
dept.setDeptNo((short)50);
dept.setDeptName("产品部");
dept.setLocation("北京");
//建立Emp到Dept的单向关联即可
emp.setDept(dept);
EmpBiz empBiz = new EmpBiz();
//用主动维护关系的一方来插入数据 以保证外键关系的正常维护
empBiz.addNewEmp(emp);
}
注意:因为一端维护关系另一端不维护关系的原因,我们必须注意避免在应用中用不维护关系的类(Dept)建立关系,因为这样建立的关系是不会在数据库中存储的。
运行测试方法 ,
Hibernate
将保存Emp
对象 , 并级联保存Dept
对象