引言
最近看到了实体图相关的博客,国外老哥说 JPA
的实体间关联会执行多次查询,当数据量大时性能非常差。
正好实体图在之前写华软的时候就学习过,只是没学明白,没在项目中实际应用,正好借此机会学习一下。
实践出真知,建一个 jpa
的项目,实际测试一下 jpa
到底是怎么查询的。
今日才发现,Spring Boot
都更新到了2.1.5
。
探究
实体关系
最简单的教务系统模型,教师、班级、学生。
基础数据
两个教师,一个教师带俩班,一个班里俩学生。
单表查询
Iterable<Teacher> teachers = teacherRepository.findAll();
for (Teacher teacher : teachers) {System.out.println(teacher.getName());
}
最简单的查询语句,findAll
之后获取 name
字段。
Hibernate: select teacher0_.id as id1_2_, teacher0_.name as name2_2_ from teacher teacher0_
简单的单表查询,查出了基础字段 id
和name
。
OneToMany
多表关联查询
Iterable<Teacher> teachers = teacherRepository.findAll();
for (Teacher teacher : teachers) {System.out.println(teacher.getName());
for (Klass klass : teacher.getKlasses()) {System.out.println(klass.getName());
for (Student student : klass.getStudents()) {System.out.println(student.getName());
}
}
}
看着挺普通的一段代码,大家应该都写过,其实里面大有学问。
测试环境
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.yunzhiclub.jpa.entity.Teacher.klasses, could not initialize proxy - no Session
测试环境运行时报错,no session
。
在 Hibernate
中,所有对数据库的操作都需要通过 session
来执行,大家可以把它理解为一个数据库的代理对象,通过调用它的方法替代了对数据库的直接操作。
关于 Hibernate
中的 Session
和JDBC
中 Connection
区别,可以参考这个老哥的回答:What is the difference between a Session and a Connection in Hibernate? – StackOverflow
因为 OneToMany
默认是惰性加载,用到的时候再去查询。
这里报错了,说明在单元测试中 session
的作用域只有一行。findAll
之后 session
就关了。
解决方案
两种解决方案,但都不是最佳实践。
加事务
加事务确实能解决 session
关闭的问题。因为这里是单元测试,也确实需要事务,所以在单元测试中的惰性加载引发的问题,我们采用加事务的方式实现。
没有查到原因,但是这里猜想了一下:
Hibernate
说到底就是把原生的 jdbc
方法进行了封装,原理就是生成如下的代码。
Session session = factory.openSession();
Transaction tx = null;
try {tx = session.beginTransaction();
// do some work
...
tx.commit();} catch (Exception e) {if (tx != null) tx.rollback();
e.printStackTrace();} finally {session.close();
}
可以看到,为了保持事务的正常运行,事务的提交和回滚都是在 session
的有效范围之内的,换句话说,session
是在事务完成之后才关闭的,在事务的管理下运行方法,自然能正常使用 session
了。
EAGER
加载
EAGER
意为急切的,就是一次都查出来了,这里也不敢瞎翻译,意会即可。
/**
* 教师所管理的班级
*/
@OneToMany(mappedBy = "teacher",fetch = FetchType.EAGER)
private List<Klass> klasses = new ArrayList<>();
/**
* 本班级中的学生
*/
@OneToMany(mappedBy = "klass", fetch = FetchType.EAGER)
private List<Student> students = new ArrayList<>();
这样可以实现,但是这是最差的一种解决方案。
因为是在实体的注解上加的,所以不同的方法用,不管用没用到关联的实体,一次性全都查出来。
我们知道 一对多
和多对多
查询是是需要编辑整个数据表的,所以不好查,改成 EAGER
后会有严重的性能问题。
Hibernate
注解设计
一起去看看 Hibernate
对注解的设计:
多对多注解,默认LAZY
:
一对多注解,默认LAZY
:
多对一注解,默认EAGER
:
一对一注解,默认EAGER
:
在数据库领域,Hibernate
肯定是大牛,既然大牛这么设计,我们自然应该也遵循。
同时应该也明白,在性能方面:一对一
、 多对一
查询性能好,多对一
、 多对多
查询性能就没那么好了。
运行环境
把同样的代码放到 Service
里,然后再做成 api
接口。
然后就正常的运行,用到的时候再去查询数据库,没有发生 no session
的错误,所以这里猜想,在 Service
中的查询方法,其 session
的作用域肯定要比测试中的要广。
问题复现
我在华软中就遇到了 no session
的问题,怎么出现的呢?
loadUserByUsername
方法中根据 username
查user
,然后把 user
传给 createUser
方法去处理,然后又把 user
传给了 getAllAuthMenuByUser
方法获取这个用户的所有授权菜单。
然后就 no session
了。
我当时想的就是方法之间相互传对象然后 session
不知怎么就关了,然后就报错了。因为我记得当时把 createUser
里的代码都放进 loadUserByUsername
里就正常了,但是改之后太长,就没有这么改。最后在方法上加的事务。
然后我就开始尝试,开始在 Service
之间互相传对象,可惜,怎么传都好使。问题复现失败,不知道是不是和 Spring Security
有什么关系?
总结与思考
网上有的文章说:多条语句的性能不好。根据教师带出学生和班级,需要执行多条 SQL
语句,可以将其优化为一条提高查询效率。
我阐述一下自己的观点:一对多慢,不是因为执行了多少条语句,而是数据库去查一对多、多对多就慢。
即便可以使用实体图优化为一条SQL
,但是数据库该怎么查一对多、多对多还是怎么查,只是看上去语句少了,我觉得性能其实也没什么提升。
实体图其实就是配置了一下我这个方法要查出什么,比如配置上我要教师的name
、班级的name
、学生的name
,然后就像我们数据库里学的那样。
select teacher.name, klass.name, student.name where xxxxx;
随着 Java
的蓬勃发展,Java
的性能虽然比不上 C++
,但运行效率也是极高。再去看效率瓶颈,还是数据库。要不怎么有的Redis
呢?