最近在进行数据统计查询时屡次遇到慢查询事件,最终发现问题发生在
hibernate
的查询操作上。
hibernate
中
@ManyToOne
注解上的
FetchType
默认值为
FetchType.EAGER
,在进行查询操作时,
hibernate
会自动的发起关联表的
join
查询。一旦关联的表太多则会大幅地影响查询效率。
在简单的数据查询中,上述查询机制并无可厚非:此机制能够在查询某个实体时,自动关联查询相关实体,这使得程序开发变得异常简单。但正是由于此方法会关联查询出过多的信息,使得在进行大量的数据操作时给数据库带来了过多的压力,数据库不堪重负,随之带来慢查询。
解决由于关联查询造成的慢查询问题的方法有几个:比如牺牲部分便利性为
@ManyToOne
注解添加
fetch = FetchType.LAZY
属性;再比如可以用综合查询专门的创建一个视图,并在综合查询中调用视图中的数据;再比如还可以为综合查询专门建立一个返回值类型。
本文给出一种通过代码来定义返回的字段、自动去除无用的关联查询的方法。
假设有以下 4 张表,分别为学生、班级、教师、学校。每个表中均有两个字段,分别为 id 及 name。er 图如下:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY ➊)
private Long id;
private String name;
@ManyToOne(cascade = CascadeType.PERSIST ➋)
private Clazz clazz;
// 省略空构造函数★及setter/getter
}
[success] 班级、教师、学校三个实体的代码均参考上述代码完成。
public interface StudentRepository extends CrudRepository<Student, Long>, JpaSpecificationExecutor {
}
@SpringBootTest
class StudentRepositoryTest {
@Autowired
StudentRepository studentRepository;
@Autowired
private EntityManager entityManager; ➊
Student student;
@BeforeEach ➋
public void beforeEach() {
School school = new School();
school.setName("测试学校");
Teacher teacher = new Teacher();
teacher.setName("测试教师");
Clazz clazz = new
Clazz();
clazz.setName("测试班级");
this.student = new Student();
student.setName("测试学生");
teacher.setSchool(school);
clazz.setTeacher(teacher);
student.setClazz(clazz);
this.studentRepository.save(student);
}
@Test
public void find() {
this.studentRepository.findById(student.getId()).get();
}
}
-
-
老的版本中使用的是
@Befor
e
,具体请参数本文给出的 github 链接
select
student0_.id as id1_2_0_, student0_.clazz_id as clazz_id3_2_0_, student0_.name as name2_2_0_,
clazz1_.id as id1_0_1_, clazz1_.name as name2_0_1_, clazz1_.teacher_id as teacher_3_0_1_,
teacher2_.id as id1_3_2_, teacher2_.name as name2_3_2_, teacher2_.school_id as school_i3_3_2_,
school3_.id as id1_1_3_, school3_.name as name2_1_3_
from student student0_
left outer join clazz clazz1_ on student0_.clazz_id=clazz1_.id
left outer join teacher teacher2_ on clazz1_.teacher_id=teacher2_.id
left outer join school school3_ on teacher2_.school_id=school3_.id
where student0_.id=1
如上所示
hibernate
在查询学生时,会关联查询学生实体中通过
@ManyToOne
注解的字段,并且它还会聪明的依次累推关联查询班级实体中的教师字段以及教师实体对应的学校字段。
Hiberante
在综合中提供了
Selection
来解决查询时冗余字段与冗余关联的问题,在使用
Selection
来进行查询时需要先在实体类中建立对应的构造函数,假设当前仅需要查询出学生的
id,name
信息。则首先需要建立以下构造函数:
public Student(Long id, String name) {
this.id = id;
this.name = name;
System.out.println("student construct");
}
@Test
public void findByColumn() {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder(); ➊
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Student.class); ➊
Root root = criteriaQuery.from(Student.class); ➊
criteriaQuery
.multiselect(root.get("id"), root.get("name")) ➋
.where(criteriaBuilder.equal(root.get("id").as(Long.class), student.getId().toString())); ➌
TypedQuery query = this.entityManager.createQuery(criteriaQuery); ➍
List students = query.getResultList(); ➎
}
}
-
创建用于综合查询的
criteriaBuilder
、
criteriaQuery
、
root
。
-
创建本次查询的输出字段为
student
实体的
id
、
name
字段。
-
-
-
select student0_.id as col_0_0_, student0_.name as col_1_0_
from student student0_
where student0_.id=1
student construct
如上所示,在综合查询中使用了
multiselect
指定输出字段后,
hibernate
进行查询时在进行
select
时只选择了规定字段
student.id、student.name
,并且在查询中并没有关联其它表。在查询出数据后,调用了
Student
实体中的构造函数。
在需要进行关联查询时仍可按上述的步骤:先建立对应的构造函数,再设置相应的选择条件。比如需要查询出班级
id
及教师
id
的信息,代码如下:
public Student(Long id, String name, Long clazzId, Long teacherId) {
this.id = id;
this.name = name;
this.clazz = new Clazz();
this.clazz.setId(clazzId);
this.clazz.setTeacher(new Teacher());
this.clazz.getTeacher().setId(teacherId);
System.out.println("student construct invoked");
}
@Test
public void findByColumnWithJoin() {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Student.class);
Root root = criteriaQuery.from(Student.class);
criteriaQuery
.multiselect(root.get("id"),
root.get("name"),
root.get("clazz").get("id"),
root.get("clazz").get("teacher").get("id"))
.where(criteriaBuilder.equal(root.get("id").as(Long.class), student.getId().toString()));
TypedQuery query = this.entityManager.createQuery(criteriaQuery);
List students = query.getResultList();
}
select student0_.id as col_0_0_, student0_.name as col_1_0_, student0_.clazz_id as col_2_0_,
clazz1_.teacher_id as col_3_0_
from student student0_
cross join clazz clazz1_
where student0_.clazz_id=clazz1_.id and student0_.id=1
student construct invoked
如上所示
hibrenate
自动构建了有需要级联
sql
语句。
如果不想使用添加构造函数的方法来进行查询,还可以使用
Selection
。仍与上述查询为例:使用
Sel
ection
进行查询的代码如下: