hibernate查詢時指定查詢字段、級聯表的一種方式

最近在進行數據統計查詢時多次遇到慢查詢事件,最終發現問題發生在hibernate的查詢操做上。hibernate@ManyToOne註解上的FetchType默認值爲FetchType.EAGER,在進行查詢操做時,hibernate會自動的發起關聯表的join查詢。一旦關聯的表太多則會大幅地影響查詢效率。java

在簡單的數據查詢中,上述查詢機制並沒有可厚非:此機制可以在查詢某個實體時,自動關聯查詢相關實體,這使得程序開發變得異常簡單。但正是因爲此方法會關聯查詢出過多的信息,使得在進行大量的數據操做時給數據庫帶來了過多的壓力,數據庫不堪重負,隨之帶來慢查詢。解決因爲關聯查詢形成的慢查詢問題的方法有幾個:好比犧牲部分便利性爲@ManyToOne註解添加fetch = FetchType.LAZY屬性;再好比能夠用綜合查詢專門的建立一個視圖,並在綜合查詢中調用視圖中的數據;再好比還能夠爲綜合查詢專門創建一個返回值類型。git

本文給出一種經過代碼來定義返回的字段、自動去除無用的關聯查詢的方法。github

情景設置

假設有如下4張表,分別爲學生、班級、教師、學校。每一個表中均有兩個字段,分別爲idname。er圖以下:spring

image.png

數據表間的關係均爲n:1,示例實體以下:sql

@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語句以下:mybatis

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 本文示例代碼

做者:河北工業大學夢雲智開發團隊 潘傑

相關文章
相關標籤/搜索