SpringData JPA中儲存後重新整理並獲取實體

banq發表於2024-03-20

Java Persistence API (JPA) 充當 Java 物件和關聯式資料庫之間的橋樑,使我們能夠無縫地持久儲存和檢索資料。在本教程中,我們將探索在 JPA 中儲存操作後有效重新整理和獲取實體的各種策略和技術。

什麼是Spring Data JPA中的實體管理?
在Spring Data JPA中,實體管理圍繞JpaRepository介面展開,該介面充當與資料庫互動的主要機制。透過擴充套件CrudRepository的 JpaRepository介面,Spring Data JPA 提供了一組強大的實體持久化、檢索、更新和刪除方法。

此外,entityManager由 Spring 容器自動注入到這些儲存庫介面中。該元件是嵌入在 Spring Data JPA 中的 JPA 基礎設施的一個組成部分,促進與底層持久化上下文的互動以及 JPA 查詢的執行。

持久化上下文
JPA 中的一個關鍵元件是永續性上下文。將此上下文想象為一個臨時儲存區域,JPA 在此管理檢索或建立的實體的狀態。
它確保:

  • 實體是唯一的:在任何給定時間,上下文中僅存在一個具有特定主鍵的實體例項。
  • 跟蹤更改:EntityManager跟蹤上下文中對實體屬性所做的任何修改。
  • 保持資料一致性:EntityManager在事務期間將上下文中所做的更改與底層資料庫同步。

JPA 實體的生命週期
JPA 實體有四個不同的生命週期階段:新建、託管、刪除和分離。

當我們使用實體的建構函式建立一個新的實體例項時,它處於“新建”狀態。我們可以透過檢查實體的 ID(主鍵)是否為空來驗證這一點:

Order order = new Order();
if (order.getId() == null) {
    <font>// Entity is in the "New" state<i>
}

當我們使用儲存庫的save()方法持久化實體後,它會轉換為“託管”狀態。我們可以透過檢查儲存庫中是否存在儲存的實體來驗證這一點:

Order savedOrder = repository.save(order);
if (repository.findById(savedOrder.getId()).isPresent()) {
    <font>// Entity is in the "Managed" state<i>
}

當我們在託管實體上呼叫儲存庫的delete()方法時,它會轉換為“已刪除”狀態。我們可以透過檢查刪除後實體是否不再存在於資料庫中來驗證這一點:

repository.delete(savedOrder);
if (!repository.findById(savedOrder.getId()).isPresent()) {
    <font>// Entity is in the "Removed" state<i>
}

最後,一旦使用儲存庫的detach()方法分離實體,該實體就不再與永續性上下文關聯。對分離實體所做的更改不會反映在資料庫中,除非顯式合併回託管狀態。我們可以透過在分離實體後嘗試修改實體來驗證這一點:

repository.detach(savedOrder);
<font>// Modify the entity<i>
savedOrder.setName(
"New Order Name");

如果我們對分離的實體呼叫save(),它會將該實體重新附加到永續性上下文,並在重新整理永續性上下文時將更改持久化到資料庫。

1、使用 Spring Data JPA 儲存實體
當我們呼叫save()時,Spring Data JPA 會安排實體在事務提交時插入資料庫。它將實體新增到永續性上下文中,將其標記為託管。

下面是一個簡單的程式碼片段,演示瞭如何使用Spring Data JPA 中的save()方法來持久儲存實體:

Order order = new Order();
order.setName(<font>"New Order Name");
repository.save(order);

但是,需要注意的是,呼叫save()不會立即觸發資料庫插入操作。相反,它只是將實體轉換為永續性上下文中的託管狀態。因此,如果其他事務在我們的事務提交之前從資料庫讀取資料,它們可能會檢索到過時的資料,其中不包括我們已進行但尚未提交的更改。

為了確保資料保持最新,我們可以採用兩種方法:獲取和重新整理。

2、在 Spring Data JPA 中獲取實體
當我們獲取一個實體時,我們不會丟棄在永續性上下文中對其進行的任何修改。相反,我們只是從資料庫中檢索實體的資料並將其新增到持久化上下文中以進行進一步處理。

2.1. 使用findById()
Spring Data JPA 儲存庫提供了諸如findById()之類的便捷方法來檢索實體。 這些方法始終從資料庫中獲取最新資料,無論永續性上下文中實體的狀態如何。這種方法簡化了實體檢索並消除了直接管理永續性上下文的需要。

Order order = repository.findById(1L).get();

2.2. 急切與懶惰獲取
在 急切獲取中,與主實體關聯的所有相關實體與主實體同時從資料庫中檢索。 透過在orderItems集合上設定fetch = FetchType.EAGER ,我們指示 JPA 在檢索Order時立即獲取所有關聯的OrderItem實體:

@Entity
public class Order {
    @Id
    private Long id;
    @OneToMany(mappedBy = <font>"order", fetch = FetchType.EAGER)
    private List<OrderItem> orderItems;
}

這意味著在findById()呼叫之後,我們可以直接訪問訂單物件中的orderItems列表,並迭代關聯的OrderItem實體,而不需要任何額外的資料庫查詢:

Order order = repository.findById(1L).get();
<font>// Accessing OrderItems directly after fetching the Order<i>
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 = <font>"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(); <font>// This triggers a separate query to fetch OrderItems<i>
    for (OrderItem item : items) {
        System.out.println(
"Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
    }
}

2.3. 使用 JPQL 獲取
Java 永續性查詢語言(JPQL) 允許我們編寫針對實體而不是表的類似 SQL 的查詢。它提供了根據各種標準檢索特定資料或實體的靈活性。

讓我們看一個按客戶名稱獲取訂單且訂單日期在指定範圍內的示例:

@Query(<font>"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);

3.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(<font>"customerName"), customerName);
Predicate dateRangePredicate = criteriaBuilder.between(root.get(
"orderDate"), startDate, endDate);
criteriaQuery.where(customerPredicate, dateRangePredicate);
return entityManager.createQuery(criteriaQuery).getResultList();

3、使用 Spring Data JPA 重新整理實體
重新整理 JPA 中的實體可確保應用程式中實體的記憶體表示與資料庫中儲存的最新資料保持同步。當其他事務修改或更新實體時,永續性上下文中的資料可能會變得過時。重新整理實體使我們能夠從資料庫中檢索最新的資料,防止不一致並保持資料的準確性。

3.1. 使用refresh()
在JPA中,我們使用EntityManager提供的refresh()方法來實現實體重新整理。在託管實體上呼叫refresh()會丟棄在永續性上下文中對該實體所做的任何修改。它從資料庫重新載入實體的狀態,有效地替換自實體上次與資料庫同步以來所做的任何修改。

但是,請務必注意 Spring Data JPA 儲存庫不提供內建的重新整理()方法。

以下是使用EntityManager重新整理實體的方法:

@Autowired
private EntityManager entityManager;
entityManager.refresh(order);

3.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) {
        <font>// Refresh the entity and potentially retry the update<i>
        entityManager.refresh(order);
       
// Consider adding logic to handle retries or notify the user about the conflict<i>
    }
}

此外,值得注意的是,如果自上次檢索以來正在重新整理的實體已被另一個事務從資料庫中刪除,則refresh()可能會丟擲javax.persistence.EntityNotFoundException 。

相關文章