開發者實作 Department(一) ↔ Employee(多) 雙向關聯,保存正常但讀取拋出 StackOverflowError,錯誤堆疊指向 Employee.toString() 循環調用。本文深度解析循環引用成因與多種解決方案。
java.lang.StackOverflowError
at org.xxf.OneToManyA.Employee.toString(Employee.java:13)
at java.util.AbstractCollection.toString(AbstractCollection.java:422)
at org.hibernate.collection.internal.PersistentSet.toString(PersistentSet.java:327)
// 無限循環...
// Department 端
@OneToMany(mappedBy = "department")
private Set employees = new HashSet<>();
// Employee 端
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// Employee.java 第 13 行附近
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name +
", department=" + department + "}"; // ← 這裡!
}
// Department.toString() 同理包含 employees 集合
// 呼叫鏈:Employee.toString() → Department.toString() →
// Set.toString() → Employee.toString() → ... 無限遞迴
// Employee
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name + "'}"; // 移除 department
}
// Department
@Override
public String toString() {
return "Department{id=" + id + ", name='" + name + "'}"; // 移除 employees
}
@Data
@Entity
public class Employee {
@Id @GeneratedValue
private Integer id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
@ToString.Exclude // ← 關鍵註解
private Department department;
}
@Data
@Entity
public class Department {
@Id @GeneratedValue
private Integer id;
private String name;
@OneToMany(mappedBy = "department")
@ToString.Exclude // ← 雙向都排除
private Set employees = new HashSet<>();
}
// Employee
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name +
", departmentId=" + (department != null ? department.getId() : null) + "}";
}
// Department
@Override
public String toString() {
return "Department{id=" + id + ", name='" + name +
", employeeCount=" + (employees != null ? employees.size() : 0) + "}";
}
// 僅解決 JSON 序列化循環,不解決 toString() 循環
@JsonManagedReference
@OneToMany(mappedBy = "department")
private Set employees;
@JsonBackReference
@ManyToOne
private Department department;
toString() 循環@OneToMany(fetch = FetchType.LAZY) (預設即為 LAZY)| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 使用 Lombok 專案 | @ToString.Exclude | 最乾淨、編譯期保證、一行解決 |
| 無 Lombok 傳統專案 | 手寫 toString 排除關聯 | 零依賴、完全可控 |
| 快速原型/教學專案 | 移除關聯 toString | 最快見效、理解核心概念 |
| 需除錯看關聯 | 手寫 toString 顯示 ID/計數 | 平衡除錯需求與安全性 |
// ❌ 危險:若包含關聯物件,同樣會循環
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee that = (Employee) o;
return Objects.equals(id, that.id) &&
Objects.equals(department, that.department); // 循環!
}
// ✅ 安全:僅用主鍵或業務鍵
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee that = (Employee) o;
return Objects.equals(id, that.id); // 僅比較 ID
}
@Override
public int hashCode() {
return Objects.hash(id);
}
StackOverflowError 在 Hibernate 雙向關聯中極常見,根因是 toString() (或 equals()/hashCode()) 觸發雙向導航循環。**核心原則:實體類的 toString/equals/hashCode 絕不包含關聯物件**。現代專案首選 Lombok @ToString.Exclude,傳統專案手寫排除關聯欄位,徹底從源頭斷開循環鏈。