SpringData JPA中保存后刷新并获取实体

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) {
    // 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(),它会将该实体重新附加到持久性上下文,并在刷新持久性上下文时将更改持久化到数据库。

1、使用 Spring Data JPA 保存实体
当我们调用save()时,Spring Data JPA 会安排实体在事务提交时插入数据库。它将实体添加到持久性上下文中,将其标记为托管。

下面是一个简单的代码片段,演示了如何使用Spring Data JPA 中的save()方法来持久保存实体:

Order order = new Order();
order.setName("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 = "order", fetch = FetchType.EAGER)
    private List<OrderItem> orderItems;
}

这意味着在findById()调用之后,我们可以直接访问订单对象中的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());
    }
}

2.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);

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("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) {
        // 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 。