前言
先用我不是藥神電影海報鎮樓,這個電影真心不錯,推薦大家。
準備
講解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);
}
複製程式碼
我們可以將迴圈引用的問題定位到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()方法。以上就是迴圈引用的過程。
我們將@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));
}
}
複製程式碼
檢視反編譯後的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;
複製程式碼
尾言
在沒有真正理解框架幹了什麼之前,不要對框架充分信任。我們要明白Lombok框架幹了什麼,不然出現一堆問題就懵逼了。