3

最近在进行数据统计查询时屡次遇到慢查询事件,最终发现问题发生在hibernate的查询操作上。hibernate@ManyToOne注解上的FetchType默认值为FetchType.EAGER,在进行查询操作时,hibernate会自动的发起关联表的join查询。一旦关联的表太多则会大幅地影响查询效率。

在简单的数据查询中,上述查询机制并无可厚非:此机制能够在查询某个实体时,自动关联查询相关实体,这使得程序开发变得异常简单。但正是由于此方法会关联查询出过多的信息,使得在进行大量的数据操作时给数据库带来了过多的压力,数据库不堪重负,随之带来慢查询。解决由于关联查询造成的慢查询问题的方法有几个:比如牺牲部分便利性为@ManyToOne注解添加fetch = FetchType.LAZY属性;再比如可以用综合查询专门的创建一个视图,并在综合查询中调用视图中的数据;再比如还可以为综合查询专门建立一个返回值类型。

本文给出一种通过代码来定义返回的字段、自动去除无用的关联查询的方法。

情景设置

假设有以下4张表,分别为学生、班级、教师、学校。每个表中均有两个字段,分别为idname。er图如下:

image.png

数据表间的关系均为n:1,示例实体如下:

@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();
    }
}
  • ➊ 备用
  • ➋ 老的版本中使用的是@Before,具体请参数本文给出的github链接

生成的sql语句如下:

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注解的字段,并且它还会聪明的依次累推关联查询班级实体中的教师字段以及教师实体对应的学校字段。

Selection<Tuple>

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<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class); ➊
        Root<Student> 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<Student> query = this.entityManager.createQuery(criteriaQuery); ➍

        List<Student> 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.idstudent.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<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class);
        Root<Student> 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<Student> query = this.entityManager.createQuery(criteriaQuery);

        List<Student> 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< Tuple >

如果不想使用添加构造函数的方法来进行查询,还可以使用Selection<Tuple>。仍与上述查询为例:使用Selection<Tuple>进行查询的代码如下:

    @Test
    public void findByColumnWithJoinAndTuple() {
        CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
        CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createQuery(Tuple.class); ➊
        Root<Student> 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<Tuple> query = this.entityManager.createQuery(criteriaQuery); ➋

        List<Tuple> tuples = query.getResultList();

        List<Student> students = new ArrayList<>(); 
        tuples.forEach(tuple -> { 
            Student student = new Student();
            student.setId((Long) tuple.get(0)); ➌
            student.setName((String) tuple.get(1)); ➌
            student.setClazz(new Clazz());
            student.getClazz().setId((Long) tuple.get(2)); ➌
            student.getClazz().setTeacher(new Teacher());
            student.getClazz().getTeacher().setId((Long) tuple.get(3)); ➌
            students.add(student);
        });

    }
  • ➊ CriteriaQuery泛型使用Tuple
  • ➋ 预查询时泛型同样使用Tuple
  • ➌ 使用tuple.get(index)方法以及类型强制转换将返回的Tuple类型的数据转换为Student

控制台主要信息如下:

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

生成的sql代码仍然言简意赅。

注意事项

由于此查询方法在查询过程中使用了hardCode格式的字符串(比如root.get("id")),此字符串依赖于实体结构。实体结构发生变化后Spring JPA并不会在系统启动时有任何的错误产生,而一旦调用了相关的查询方法便会由于该字符串与实体类不对应造成系统500错误。所以在使用此查询方法时,必须结合单元测试来使用!

总结

hibernate是款优秀的ORM框架,是spring jpa的默认选型。团队一直遵从站在巨人的肩膀上,相信巨人的选择都是对的原则,在生产项目的选型上全部毫不犹豫的选择了hibernate。但近期生产项目中的一些统计查询工作它的表现却不如人意,与手写sql相比有着较大的差距。因此,开始对hibernate产生怀疑的同时近一步的加深了对其深入的学习。在此期间还学习了很少的mybatis的相关知识。
本文结论:正确合理的使用hibernate,不论是在新增、更新、删除数据,还要是批量删除、综合查询数据上,hibernate都具有在牺牲少量可控性能的前提下达到快速、便捷、面向对象的开发特点,应当成为中小型项目的首选。

参考文档

序号 链接
1 https://www.objectdb.com/java/jpa/query/jpql/select
2 本文示例代码

作者:河北工业大学梦云智开发团队 潘杰


潘杰
3.1k 声望238 粉丝

引用和评论

0 条评论