2

引言

最近看到了实体图相关的博客,国外老哥说JPA的实体间关联会执行多次查询,当数据量大时性能非常差。

正好实体图在之前写华软的时候就学习过,只是没学明白,没在项目中实际应用,正好借此机会学习一下。

clipboard.png

实践出真知,建一个jpa的项目,实际测试一下jpa到底是怎么查询的。

今日才发现,Spring Boot都更新到了2.1.5

clipboard.png

探究

实体关系

最简单的教务系统模型,教师、班级、学生。

clipboard.png

基础数据

两个教师,一个教师带俩班,一个班里俩学生。

clipboard.png

clipboard.png

clipboard.png

单表查询

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_

简单的单表查询,查出了基础字段idname

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中的SessionJDBCConnection区别,可以参考这个老哥的回答:What is the difference between a Session and a Connection in Hibernate? - StackOverflow

clipboard.png

因为OneToMany默认是惰性加载,用到的时候再去查询。

这里报错了,说明在单元测试中session的作用域只有一行。findAll之后session就关了。

clipboard.png

解决方案

两种解决方案,但都不是最佳实践。

加事务

加事务确实能解决session关闭的问题。因为这里是单元测试,也确实需要事务,所以在单元测试中的惰性加载引发的问题,我们采用加事务的方式实现。

clipboard.png

没有查到原因,但是这里猜想了一下:

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<>();

clipboard.png

这样可以实现,但是这是最差的一种解决方案。

因为是在实体的注解上加的,所以不同的方法用,不管用没用到关联的实体,一次性全都查出来。

我们知道一对多多对多查询是是需要编辑整个数据表的,所以不好查,改成EAGER后会有严重的性能问题。

Hibernate注解设计

一起去看看Hibernate对注解的设计:

多对多注解,默认LAZY

clipboard.png

一对多注解,默认LAZY

clipboard.png

多对一注解,默认EAGER

clipboard.png

一对一注解,默认EAGER

clipboard.png

在数据库领域,Hibernate肯定是大牛,既然大牛这么设计,我们自然应该也遵循。

同时应该也明白,在性能方面:一对一多对一查询性能好,多对一多对多查询性能就没那么好了。

运行环境

把同样的代码放到Service里,然后再做成api接口。

clipboard.png

然后就正常的运行,用到的时候再去查询数据库,没有发生no session的错误,所以这里猜想,在Service中的查询方法,其session的作用域肯定要比测试中的要广。

clipboard.png

问题复现

我在华软中就遇到了no session的问题,怎么出现的呢?

clipboard.png

loadUserByUsername方法中根据usernameuser,然后把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呢?


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。


引用和评论

0 条评论