JPA-查询问题探究

42次阅读

共计 3073 个字符,预计需要花费 8 分钟才能阅读完成。

引言

最近看到了实体图相关的博客,国外老哥说 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_

简单的单表查询,查出了基础字段 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

因为 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方法中根据 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 呢?

正文完
 0