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

前言

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

image.png

準備

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

@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策略github

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

Fetch用於關聯關係,做用域爲讀取操做 @OneToMany默認的是FetchType.LAZY(懶加載) @ManyToOne默認的是FetchType.EAGER(急加載)spring

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

@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屬性。apache

爲了讓主表知道從表中的那些字段關聯本身,在主表一方能夠用mappedBy指向從表中的一個關聯到本身的對象。在從表一方能夠用@JoinColumn註解之外鍵字段的形式關聯到主表。tomcat

Cascade用於級聯,做用域爲增刪改操做。CascadeType.ALL包含全部級聯策略。(後面會具體演示不一樣級聯策略的效果,加深理解)bash

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()方法形成的死循環

咱們去查詢一個學生,看其不然用了懶加載策略session

@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框架幹了什麼,否則出現一堆問題就懵逼了。

相關文章
相關標籤/搜索