防止 Jackson 取得惰性實體字段
1. 簡介
在本快速教程中,我們將探索如何使用 Baeldung University 的領域模型來避免在未取得的惰性物件上進行 Jackson 序列化。我們將建立一個基於 Spring 的簡單應用程式來演示相關原理,無需任何 Spring 經驗即可繼續學習。所有範例僅基於 JPA 概念。
2. 設定範例
使用 JPA 和 Jackson 時,一個常見的挑戰是序列化包含惰性關聯的實體。我們先從一個包含許多課程的大學系的表示開始:
@Entity
class Department {
@Id
Long id;
String name;
@OneToMany(mappedBy = "department")
List<Course> courses;
// getters and setters
}
與courses
的關係由Course
中的department
欄位對應。我們也必須明確將取得類型設為LAZY
,因為多對一關係的預設值是EAGER
:
@Entity
class Course {
@Id
Long id;
String name;
@ManyToOne(fetch = FetchType.LAZY)
Department department;
// getters and setters
}
Jackson 序列化實體時,會呼叫其所有 getter 方法。如果某個欄位仍處於延遲載入狀態,則會觸發額外的資料庫查詢。如果持久化上下文已關閉,序列化將失敗並拋出LazyInitializationException
。
3. 演示問題
為了形象化地展示這個問題,我們將建立端點來保存和檢索Department
和Course
實體。
3.1. 測試DepartmentController
讓我們從具有簡單 POST 和 GET 端點的控制器開始:
@RestController
@RequestMapping("/departments")
public class DepartmentController {
@Autowired
DepartmentRepository repository;
@PostMapping
Department post(@RequestBody Department department) {
return repository.save(department);
}
@GetMapping("/{id}")
Optional<Department> get(@PathVariable("id") Long id) {
return repository.findById(id);
}
}
即使我們不包含任何課程,嘗試檢索新建立的部門也會導致異常:
curl http://localhost:8080/departments/1
這是因為 Jackson 未能初始化代理字段:
failed to lazily initialize a collection of role:
com.baeldung.jacksonlazyfields.model.Department.courses:
could not initialize proxy - no Session
3.2. 測試CourseController
我們將遵循相同的課程控制器模式,使用簡單的 POST 和 GET 端點。但這次,我們只會在課程包含系所時收到異常。我們可以透過插入新的系所來檢查:
curl -X POST http://localhost:8080/departments\
-H "Content-Type: application/json" \
-d '{"name":"Computer-Science"}'
然後,透過 ID 在新課程中引用它:
curl -X POST http://localhost:8080/courses \
-H "Content-Type: application/json" \
-d '{"name":"Machine-Learning", "department":{"id":1} }'
如果我們嘗試檢索該課程,我們將收到與第一個類似的異常:
could not initialize proxy [com.baeldung.jacksonlazyfields.model.Department#1]
- no Session
讓我們看看有哪些選擇可以避免這種情況。
4. 使用@JsonIgnore
如果我們不需要序列化特定的關聯,一個簡單的選擇是在欄位上包含一個@JsonIgnore
註解:
@JsonIgnore
@OneToMany(mappedBy = "department")
List<Course> courses;
現在,該欄位被排除在序列化之外:
{
"id": 1,
"name": "Computer-Science"
}
這對於隱藏有效載荷中的欄位很有用。
5.使用 Jackson 的 Hibernate 模組
一個全域解決方案是為 Jackson 註冊 Hibernate 模組,該模組依賴jackson-datatype-hibernate6
:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate6</artifactId>
</dependency>
在我們的 POM 中包含依賴項之後,我們必須確保我們正在使用的ObjectMapper
實例註冊了Hibernate6Module
:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate6Module());
這避免了延遲載入;相反,Jackson 現在對未獲取的關聯寫入null
:
{
"id": 1,
"name": "Computer-Science",
"courses": null
}
這個解決方案很好,因為它適用於使用該ObjectMapper
實例序列化的任何物件。
6.使用 DTO 投影
一種繁瑣但強大的方法是創建 DTO。這樣,我們可以完全控制序列化的內容,同時解耦持久層,避免引入外部函式庫。
6.1. 建立 DTO
首先,讓我們建立一個僅包含我們想要序列化的欄位的記錄:
public record DepartmentDto(Long id, String name) {}
6.2. 更新 GET 端點
其次,我們將更新 GET 端點以使用 DTO:
@GetMapping("/{id}")
Optional get(@PathVariable("id") Long id) {
return repository.findById(id)
.map(d -> new DepartmentDto(id, d.getName()));
}
6.3. 取得結果
現在,我們不會再遇到 Jackson 遇到未取得欄位的問題了:
{
"id": 1,
"name": "Computer-Science"
}
7. 結論
在本文中,我們學習瞭如何在序列化惰性關係時阻止 Jackson 存取未取得的代理。最佳方法取決於我們的設計;DTO 是最簡潔的,而註冊 Hibernate 模組可以避免意外異常。此外,只需添加@JsonIgnore
即可確保字段永遠不會被序列化。
與往常一樣,原始碼可在 GitHub 上取得。