在 JPA 中儲存後刷新並取得實體
一、簡介
Java Persistence API (JPA) 充當 Java 物件和關聯式資料庫之間的橋樑,使我們能夠無縫地持久保存和檢索資料。在本教程中,我們將探索在 JPA 中保存操作後有效刷新和獲取實體的各種策略和技術。
2.了解Spring Data JPA中的實體管理
在 Spring Data JPA 中,實體管理圍繞著JpaRepository
介面展開,該介面可作為與資料庫互動的主要機制。透過擴展CrudRepository
JpaRepository
接口,Spring Data JPA 提供了一組強大的實體持久化、檢索、更新和刪除方法。
此外, entityManager
由 Spring 容器自動注入到這些儲存庫介面中。該元件是嵌入在 Spring Data JPA 中的 JPA 基礎設施的一個組成部分,促進與底層持久化上下文的交互以及 JPA 查詢的執行。
2.1.持久化情境
JPA 中的一個關鍵元件是持久性上下文。將此上下文想像為一個臨時保存區域,JPA 在此管理檢索或建立的實體的狀態。
它確保:
- 實體是唯一的:在任何給定時間,上下文中僅存在一個具有特定主鍵的實體實例。
- 追蹤變更:
EntityManager
追蹤上下文中對實體屬性所做的任何修改。 - 保持資料一致性:
EntityManager
在交易期間將上下文中所做的變更與底層資料庫同步。
2.2. JPA 實體的生命週期
JPA 實體有四個不同的生命週期階段:新建、託管、刪除和分離。
當我們使用實體的建構函式建立一個新的實體實例時,它處於「New」狀態。我們可以透過檢查實體的 ID(主鍵)是否為空來驗證這一點:
Order order = new Order();
if (order.getId() == null) {
// Entity is in the "New" state
}
當我們使用儲存庫的save()
方法持久化實體後,它會轉換為「託管」狀態。我們可以透過檢查儲存庫中是否存在已儲存的實體來驗證這一點:
Order savedOrder = repository.save(order);
if (repository.findById(savedOrder.getId()).isPresent()) {
// Entity is in the "Managed" state
}
當我們在託管實體上呼叫儲存庫的delete()
方法時,它會轉換為「已刪除」狀態。我們可以透過檢查刪除後實體是否不再存在於資料庫中來驗證這一點:
repository.delete(savedOrder);
if (!repository.findById(savedOrder.getId()).isPresent()) {
// Entity is in the "Removed" state
}
最後,一旦使用儲存庫的detach()
方法分離實體,該實體就不再與持久性情境關聯。對分離實體所做的變更不會反映在資料庫中,除非明確合併回託管狀態。我們可以透過在分離實體後嘗試修改實體來驗證這一點:
repository.detach(savedOrder);
// Modify the entity
savedOrder.setName("New Order Name");
如果我們對分離的實體呼叫save()
,它會將該實體重新附加到持久性上下文,並在刷新持久性上下文時將變更持久化到資料庫。
3. 使用 Spring Data JPA 儲存實體
當我們呼叫save()
時,Spring Data JPA 會安排實體在交易提交時插入資料庫。它將實體新增至持久性上下文中,將其標記為託管。
下面是一個簡單的程式碼片段,示範如何使用 Spring Data JPA 中的save()
方法來持久保存實體:
Order order = new Order();
order.setName("New Order Name");
repository.save(order);
但是,需要注意的是,呼叫save()
不會立即觸發資料庫插入操作。相反,它只是將實體轉換為持久性上下文中的託管狀態。因此,如果其他事務在我們的事務提交之前從資料庫讀取數據,它們可能會檢索到過時的數據,其中不包括我們已進行但尚未提交的更改。
為了確保資料保持最新,我們可以採用兩種方法:取得和刷新。
4. 在 Spring Data JPA 中取得實體
當我們取得一個實體時,我們不會丟棄在持久性上下文中對其進行的任何修改。相反,我們只是從資料庫中檢索實體的資料並將其添加到持久化上下文中以進行進一步處理。
4.1.使用findById()
Spring Data JPA 儲存庫提供了諸如findById()
之類的便利方法來擷取實體。這些方法始終從資料庫中獲取最新數據,無論持久性上下文中實體的狀態如何。這種方法簡化了實體檢索並消除了直接管理持久性上下文的需要。
Order order = repository.findById(1L).get();
4.2.急切與懶惰獲取
在急切地獲取中,與主實體關聯的所有相關實體與主實體同時從資料庫中檢索。透過在orderItems
集合上設定fetch = FetchType.EAGER
,我們指示 JPA 在檢索Order
時立即取得所有關聯的OrderItem
實體:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderItem> orderItems;
}
這意味著在findById()
呼叫之後,我們可以直接存取order
物件中的orderItems
列表,並迭代關聯的OrderItem
實體,而不需要任何額外的資料庫查詢:
Order order = repository.findById(1L).get();
// Accessing OrderItems directly after fetching the Order
if (order != null) {
for (OrderItem item : order.getOrderItems()) {
System.out.println("Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
}
}
另一方面,透過設定fetch = FetchType.LAZY
,除非在程式碼中明確存取相關實體,否則不會從資料庫中檢索相關實體:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
}
當我們呼叫order.getOrderItems()
時,會執行一個單獨的資料庫查詢來取得該訂單的關聯OrderItem
實體。只有當我們明確存取orderItems
清單時才會觸發此附加查詢:
Order order = repository.findById(1L).get();
if (order != null) {
List<OrderItem> items = order.getOrderItems(); // This triggers a separate query to fetch OrderItems
for (OrderItem item : items) {
System.out.println("Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
}
}
4.3.使用 JPQL 獲取
Java 持久性查詢語言 (JPQL) 允許我們編寫針對實體而不是表格的類似 SQL 的查詢。它提供了根據各種標準檢索特定資料或實體的靈活性。
讓我們來看一個按客戶名稱獲取訂單且訂單日期在指定範圍內的範例:
@Query("SELECT o FROM Order o WHERE o.customerName = :customerName AND
o.orderDate BETWEEN :startDate AND :endDate")
List<Order> findOrdersByCustomerAndDateRange(@Param("customerName") String customerName,
@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);
4.4.使用 Criteria API 獲取
Spring Data JPA 中的 Criteria API 提供了一種可靠且靈活的方法來動態建立查詢。它允許我們使用方法鍊和條件表達式安全地建立複雜的查詢,確保我們的查詢在編譯時沒有錯誤。
讓我們考慮一個範例,其中我們使用 Criteria API 根據條件組合(例如客戶名稱和訂單日期範圍)來取得訂單:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Order> criteriaQuery = criteriaBuilder.createQuery(Order.class);
Root<Order> root = criteriaQuery.from(Order.class);
Predicate customerPredicate = criteriaBuilder.equal(root.get("customerName"), customerName);
Predicate dateRangePredicate = criteriaBuilder.between(root.get("orderDate"), startDate, endDate);
criteriaQuery.where(customerPredicate, dateRangePredicate);
return entityManager.createQuery(criteriaQuery).getResultList();
5. 使用 Spring Data JPA 刷新實體
刷新 JPA 中的實體可確保應用程式中實體的記憶體表示與資料庫中儲存的最新資料保持同步。當其他交易修改或更新實體時,持久性上下文中的資料可能會變得過時。刷新實體使我們能夠從資料庫中檢索最新的數據,防止不一致並保持數據的準確性。
5.1.使用refresh()
在JPA中,我們使用EntityManager
提供的refresh()
方法來實作實體刷新。在託管實體上呼叫refresh()
會丟棄在持久性上下文中對該實體所做的任何修改。它從資料庫重新載入實體的狀態,有效地替換自實體上次與資料庫同步以來所做的任何修改。
但是,請務必注意 Spring Data JPA 儲存庫不提供內建的refresh()
方法。
以下是使用EntityManager
刷新實體的方法:
@Autowired
private EntityManager entityManager;
entityManager.refresh(order);
5.2.處理**OptimisticLockException
**
Spring Data JPA 中的@Version
註解用於實現樂觀鎖定。當多個事務可能嘗試同時更新相同實體時,它有助於確保資料一致性。當我們使用@Version
時,JPA 會自動在我們的實體類別上建立一個特殊欄位(通常稱為version
)。
此欄位儲存一個整數值,表示資料庫中實體的版本:
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@Version
private Long version;
}
從資料庫檢索實體時,JPA 會主動取得其版本。更新實體後,JPA 會將持久性上下文中的實體版本與資料庫中儲存的版本進行比較。如果實體的版本不同,則表示另一個事務修改了該實體,可能導致資料不一致。
在這種情況下,JPA 會拋出異常(通常是OptimisticLockException
)來指示潛在的衝突。因此,我們可以在catch
區塊中呼叫refresh()
方法來從資料庫重新載入實體的狀態。
讓我們看一下這種方法如何運作的簡短演示:
Order order = orderRepository.findById(orderId)
.map(existingOrder -> {
existingOrder.setName(newName);
return existingOrder;
})
.orElseGet(() -> {
return null;
});
if (order != null) {
try {
orderRepository.save(order);
} catch (OptimisticLockException e) {
// Refresh the entity and potentially retry the update
entityManager.refresh(order);
// Consider adding logic to handle retries or notify the user about the conflict
}
}
此外,值得注意的是,如果自上次擷取以來正在刷新的實體已被另一個交易從資料庫中刪除,則refresh()
可能會拋出javax.persistence.EntityNotFoundException
。
六,結論
在本文中,我們了解了 Spring Data JPA 中刷新和取得實體之間的差異。取得涉及在需要時從資料庫中檢索最新資料。刷新涉及使用資料庫中的最新資料更新持久性上下文中的實體狀態。
透過策略性地利用這些方法,我們可以保持資料一致性並確保所有事務都在最新資料上運作。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。