使用Hibernate、JPA、Lombok遇到的有趣問題

markriver發表於2021-09-09

前言

先用我不是藥神電影海報鎮樓,這個電影真心不錯,推薦大家。

image.png

準備

講解Hibernate之前,首先建立兩個實體類,一個是Student類,一個School類。School和Student的關係是一對多的關係

@Entity
@Table(name = "tbl_school")
@Data
public class School {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")
    private String id;

    @Column(name = "school_name")
    private String schoolName;

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();

    @Column(name = "created_dt")
    private Date createdDt;

    @Column(name = "updated_dt")
    private Date updatedDt;

    @Column(name = "is_del")
    private String isDel;
}
複製程式碼
@Entity
@Table(name = "tbl_student")
@Data
public class Student {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")
    private String id;

    @Column(name = "student_name")
    private String studentName;

    @Column(name = "school_id", insertable = false, updatable = false)
    private String schoolId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "school_id")
    private School school;

    @Column(name = "created_dt")
    private Date createdDt;

    @Column(name = "updated_dt")
    private Date updatedDt;

    @Column(name = "is_del")
    private String isDel;

}
複製程式碼

基礎概念

主鍵採用UUID策略

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")
複製程式碼

Fetch用於關聯關係,作用域為讀取操作 @OneToMany預設的是FetchType.LAZY(懶載入) @ManyToOne預設的是FetchType.EAGER(急載入)

由於一個School有多個Student,我們可以用@OneToMany去維護這種關係。類似的還有@OneToOne、@ManyToOne,@ManyToMany這些註解。值得注意的話,mappedBy只能適用於@OneToOne,@OneToMany,@ManyToMany這些註解。mappedBy用於主表的一方。對於我們來說School就是主表,Student就是從表。一對多的關係由從表去負責維護。

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();
複製程式碼

再說說與mappedBy互斥的@JoinColumn註解,@JoinColumn用於擁有主表外來鍵的一方,也就是從表。

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "school_id")
    private School school;
複製程式碼

mappedBy屬性應該指向從表中維護與主表關係的欄位。對於School類來說,mappedBy就應該指向Student類中的school屬性。

為了讓主表知道從表中的那些欄位關聯自己,在主表一方可以用mappedBy指向從表中的一個關聯到自己的物件。在從表一方可以用@JoinColumn註解以外來鍵欄位的形式關聯到主表。

Cascade用於級聯,作用域為增刪改操作。CascadeType.ALL包含所有級聯策略。(後面會具體演示不同級聯策略的效果,加深理解)

public enum CascadeType {

    /** Cascade all operations */
    ALL,

    /** Cascade persist operation */
    PERSIST,

    /** Cascade merge operation */
    MERGE,

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since Java Persistence 2.0
     *
     */
    DETACH
}

複製程式碼

toString()方法造成的死迴圈

我們去查詢一個學生,看其否則用了懶載入策略

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }
複製程式碼

結果丟擲了這樣的異常...

org.hibernate.LazyInitializationException: could not initialize proxy - no Session

	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:148)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:266)
	at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:73)
	at cmazxiaoma.model.School_$$_jvstaa_0.toString(School_$$_jvstaa_0.java)
複製程式碼

Hibernate跟Spring整合了,Hibernate的Session就交付給Spring去管理。每次資料庫操作後,會關閉Session,當我們想要用懶載入方式去獲得資料的時候,原來的Session已經關閉,不能獲取資料,所以會丟擲這樣的異常。

我們可以通過Spring提供的OpenSessionInViewFilter去解決這種問題,將Hibernate的Session繫結到整個執行緒的Servlet過濾器去處理請求,而它必須依賴於Servlet容器,不適用於我們的單元測試。

@Configuration
public class FilterConfig {

    /**
     * 解決hibernate懶載入出現的no session問題
     * @return
     */
//    @Bean
//    public FilterRegistrationBean filterRegistrationBean() {
//        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
//        filterRegistrationBean.setFilter(new OpenSessionInViewFilter());
//        filterRegistrationBean.addInitParameter("urlPatterns", "/*");
//        return filterRegistrationBean;
//    }

    /**
     * 解決jpa 懶載入出現的no session問題
     * @return
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
        filterRegistrationBean.addInitParameter("urlPatterns", "/*");
        return filterRegistrationBean;
    }
}
複製程式碼

我們可以在application-dev.properties配置如下程式碼,就可以在Servlet容器和單元測試中使用懶載入策略了。

#將jpa的session繫結到整個執行緒的Servlet過濾器,處理請求
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
複製程式碼

注意喲,Hibernate依賴SessionFactory去建立Session例項,而JPA依賴於EntityManagerFactory去建立EntityManager例項。


解決了Could not initialize proxy - no session的異常,我們再去跑一下單元測試,出現了更大的錯誤"StackOverflowError"

java.lang.StackOverflowError
	at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:131)
	at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)
	at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:75)
	at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)
複製程式碼

我們可以通過日誌看到sql的輸出,發現了sql重複執行了好多次。以下我擷取了前10條sql記錄。

Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
複製程式碼

通過觀察發現,第一條sql是執行查詢Student的sql,第二條sql是執行查詢School的sql,第三條sql是執行School裡面所有學生的sql,第四條sql是執行查詢School的sql,後面所有的sql都是執行查詢School裡面所有學生的sql。

很明顯發生了迴圈依賴的情況。Lombok的@Data相當於@Getter、@Setter、@ToString、@EqualsAndHashCode、@RequiredArgsConstructor註解。

如果我們去掉System.out.println("student=" + student);這行程式碼,再去跑單元測試,會發現沒有報錯。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }
複製程式碼

image.png

我們可以將迴圈引用的問題定位到Student和School類的toString()方法。Lombok的@Data註解為我們生成的toString()覆蓋了整個類的屬性。

  // School類
    @Override
    public String toString() {
        return "School{" +
                "id='" + id + '\'' +
                ", schoolName='" + schoolName + '\'' +
                ", studentList=" + studentList +
                ", createdDt=" + createdDt +
                ", updatedDt=" + updatedDt +
                ", isDel='" + isDel + '\'' +
                '}';
    }

   // Student類
    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", studentName='" + studentName + '\'' +
                ", schoolId='" + schoolId + '\'' +
                ", school=" + school +
                ", createdDt=" + createdDt +
                ", updatedDt=" + updatedDt +
                ", isDel='" + isDel + '\'' +
                '}';
    }
複製程式碼

我們可以確認System.out.println("student=" + student);會呼叫Student類中toString()方法,toString()方法會觸發school屬性的懶載入,便會去呼叫School類的toString()方法,School()類中的toString()方法,會觸發studentList屬性的懶載入,接著會呼叫Student類中的toString()方法。以上就是迴圈引用的過程。

image.png

我們將@Data註解去掉,換成@Setter、@Getter、@EqualsAndHashCode註解。我們自己重寫Student類和School類的toString()方法。

   // School類
    @Override
    public String toString() {
        return "School{" +
                "id='" + id + '\'' +
                ", schoolName='" + schoolName + '\'' +
                '}';
    }

    // Student類
    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", studentName='" + studentName + '\'' +
                '}';
    }
複製程式碼

再去跑查詢Student的測試用例。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }
複製程式碼

我們發現輸出Student的資訊,並沒有去查詢School的資訊。證明懶載入策略起了作用。

Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
student=Student{id='1', studentName='捲毛'}
複製程式碼

當我們去訪問Student的School詳情資訊時,才會去查詢School資訊。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);

        School school = student.getSchool();
        System.out.println("school=" + school);
    }
複製程式碼
Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
student=Student{id='1', studentName='捲毛'}
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
school=School{id='1', schoolName='WE學校'}
複製程式碼

hashCode()方法造成的死迴圈

我們去查詢School的資訊

    @Test
    public void query() throws Exception {
        School school = schoolDao.findOne("1");
        System.out.println(school);

        Set<Student> studentList = school.getStudentList();
        System.out.println("studentList=" + studentList);
    }
複製程式碼

特麼,又發現了死迴圈。我們可以發現執行了查詢學校資訊的sql,成功輸出了學習資訊後,才發生死迴圈。

Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
School{id='1', schoolName='WE學校'}
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
複製程式碼

通過進一步,看到棧異常的錯誤定位在School類和Student類中的hashCode()。

java.lang.StackOverflowError
	at cmazxiaoma.model.School.hashCode(School.java:22)
	at sun.reflect.GeneratedMethodAccessor38.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:84)
	at cmazxiaoma.model.School_$$_jvstc33_0.hashCode(School_$$_jvstc33_0.java)
	at cmazxiaoma.model.Student.hashCode(Student.java:20)
複製程式碼

那Student和School類中的hashCode()還在什麼情況下呼叫呢? studentList是Set集合,HashSet內部實現其實是通過HashMap,HashSet的元素其實就是內部HashMap的key,HashMap的key不能重複決定了HashSet的元素不能重複。我們往HashSet裡面新增元素時,其實會呼叫hashCode()和equals()確定元素在HashMap儲存的具體位置。

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();
複製程式碼

通過反編譯School類和Student類,我們發現它們的hashCode()方法存在迴圈引用。 看School類中的hashCode()方法,studentList是一個HashSet集合,HashSet集合的hashCode()計算方式會遍歷所有元素,累加求和每個元素的hashCode值。但是studentList裡面元素的型別是Student,Student類中的hashCode()又會依賴於School類的hashCode()方法,這樣就形成了迴圈依賴。

    // School類的hashCode()方法
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $schoolName = this.getSchoolName();
        result = result * 59 + ($schoolName == null ? 43 : $schoolName.hashCode());
        Object $studentList = this.getStudentList();
        result = result * 59 + ($studentList == null ? 43 : $studentList.hashCode());
        Object $createdDt = this.getCreatedDt();
        result = result * 59 + ($createdDt == null ? 43 : $createdDt.hashCode());
        Object $updatedDt = this.getUpdatedDt();
        result = result * 59 + ($updatedDt == null ? 43 : $updatedDt.hashCode());
        Object $isDel = this.getIsDel();
        result = result * 59 + ($isDel == null ? 43 : $isDel.hashCode());
        return result;
    }
     
   // Student類中的hashCode()方法
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $studentName = this.getStudentName();
        result = result * 59 + ($studentName == null ? 43 : $studentName.hashCode());
        Object $schoolId = this.getSchoolId();
        result = result * 59 + ($schoolId == null ? 43 : $schoolId.hashCode());
        Object $school = this.getSchool();
        result = result * 59 + ($school == null ? 43 : $school.hashCode());
        Object $createdDt = this.getCreatedDt();
        result = result * 59 + ($createdDt == null ? 43 : $createdDt.hashCode());
        Object $updatedDt = this.getUpdatedDt();
        result = result * 59 + ($updatedDt == null ? 43 : $updatedDt.hashCode());
        Object $isDel = this.getIsDel();
        result = result * 59 + ($isDel == null ? 43 : $isDel.hashCode());
        return result;
    }
複製程式碼

HashSet的hashCode()方法來自與父類AbstractSet。

    public int hashCode() {
        int h = 0;
        Iterator<E> i = iterator();
        while (i.hasNext()) {
            E obj = i.next();
            if (obj != null)
                h += obj.hashCode();
        }
        return h;
    }
複製程式碼

既然發現了是@Data註解生成的hashCode()方法坑了我們,那我們自己重寫Student和Teacher類中的hashCode()和equals()方法

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof School)) return false;
        if (!super.equals(o)) return false;

        School school = (School) o;

        if (!getId().equals(school.getId())) return false;
        if (!getSchoolName().equals(school.getSchoolName())) return false;
        if (!getCreatedDt().equals(school.getCreatedDt())) return false;
        if (!getUpdatedDt().equals(school.getUpdatedDt())) return false;
        return getIsDel().equals(school.getIsDel());
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + getId().hashCode();
        result = 31 * result + getSchoolName().hashCode();
        result = 31 * result + getCreatedDt().hashCode();
        result = 31 * result + getUpdatedDt().hashCode();
        result = 31 * result + getIsDel().hashCode();
        return result;
    }
複製程式碼
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;

        Student student = (Student) o;

        if (!getId().equals(student.getId())) return false;
        if (!getStudentName().equals(student.getStudentName())) return false;
        if (!getSchoolId().equals(student.getSchoolId())) return false;
        if (!getCreatedDt().equals(student.getCreatedDt())) return false;
        if (!getUpdatedDt().equals(student.getUpdatedDt())) return false;
        return getIsDel().equals(student.getIsDel());
    }

    @Override
    public int hashCode() {
        int result = getId().hashCode();
        result = 31 * result + getStudentName().hashCode();
        result = 31 * result + getSchoolId().hashCode();
        result = 31 * result + getCreatedDt().hashCode();
        result = 31 * result + getUpdatedDt().hashCode();
        result = 31 * result + getIsDel().hashCode();
        return result;
    }
複製程式碼

記住我們重寫equals()方法,就必須要重寫hashCode()方法。可以看到Student類和School類都有id、createdDt、updatedDt、isDel的屬性,我們如果把這些相同屬性都提到父類中,讓Student類和School類繼承這個父類,同時使用@EqualsAndHashCode註解為其生成equals()和hashCode()方法。那麼會出現一個問題,在比較物件是否相等時會得出錯誤的結果。因為@EqualsAndHashCode生成的equals()和hashCode()沒有使用父類的屬性。接下來,我們就測試一下吧。


@EqualsAndHashCode的坑

定義一個Father類。

@Getter
@Setter
@EqualsAndHashCode
public class Son extends Father {

    private String sonName;

}
複製程式碼

定義一個Son類。

@Getter
@Setter
@EqualsAndHashCode
public class Son extends Father {

    private String sonName;

}
複製程式碼

我們執行下面的程式碼,比較son1和son2物件是否相等。結果返回true,很顯然只比較Son物件的屬性,沒有比較Son的父類Father裡面的屬性。

public class SonTest {

    @Test
    public void test() {
        Son son1 = new Son();
        son1.setSonName("son1");
        son1.setFatherName("baseFather");

        Son son2 = new Son();
        son2.setSonName("son1");
        son2.setFatherName("baseFather2");

        System.out.println(son1.equals(son2));

    }
}
複製程式碼

image.png

檢視反編譯後的Son類程式碼,恍然大悟。

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Son)) {
            return false;
        } else {
            Son other = (Son)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$sonName = this.getSonName();
                Object other$sonName = other.getSonName();
                if (this$sonName == null) {
                    if (other$sonName != null) {
                        return false;
                    }
                } else if (!this$sonName.equals(other$sonName)) {
                    return false;
                }

                return true;
            }
        }
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $sonName = this.getSonName();
        int result = result * 59 + ($sonName == null ? 43 : $sonName.hashCode());
        return result;
    }
複製程式碼

專案地址

會陸續更新使用Hibernate、Mybatis、JPA碰到的有趣問題,會打算從原始碼角度分析MyBatis


剛才看了評論,順便再提一下。Lombok的@EqualsAndHashCode生成的equals()和hashCode()預設是不呼叫父類的實現。 設定其屬性callSuper為true時,就可以了。

	/**
	 * Call on the superclass's implementations of {@code equals} and {@code hashCode} before calculating
	 * for the fields in this class.
	 * <strong>default: false</strong>
	 */
	boolean callSuper() default false;
複製程式碼

equals.png

hashcode.png

尾言

在沒有真正理解框架幹了什麼之前,不要對框架充分信任。我們要明白Lombok框架幹了什麼,不然出現一堆問題就懵逼了。

相關文章