1. 引言

Java持久化API(JPA)作为Java对象与关系型数据库之间的桥梁,让我们能无缝地持久化和检索数据。本文将深入探讨在JPA中保存操作后刷新和获取实体的各种策略与技巧。

2. Spring Data JPA中的实体管理

在Spring Data JPA中,实体管理围绕JpaRepository接口展开,它是与数据库交互的核心机制。通过继承CrudRepositoryJpaRepository,Spring Data JPA提供了强大的实体持久化、检索、更新和删除方法集。

Spring容器会自动将entityManager注入到这些仓库接口中。该组件是Spring Data JPA中JPA基础设施的核心部分,负责与底层持久化上下文交互并执行JPA查询。

2.1. 持久化上下文

JPA中的关键组件是持久化上下文。可以将其理解为JPA管理已检索或创建实体状态的临时缓存区,它确保:

实体唯一性:特定主键的实体实例在上下文中始终唯一
变更追踪EntityManager持续跟踪上下文中实体属性的修改
数据一致性EntityManager在事务期间将上下文变更同步到数据库

2.2. JPA实体生命周期

JPA实体有四个明确的生命周期阶段:新建(New)、托管(Managed)、删除(Removed)和游离(Detached)。

通过实体构造器创建新实例时,实体处于"新建"状态。可通过检查ID(主键)是否为null来验证:

Order order = new Order();
if (order.getId() == null) {
    // 实体处于"新建"状态
}

使用仓库的save()方法持久化实体后,它将转为"托管"状态。可通过检查仓库中是否存在该实体来验证:

Order savedOrder = repository.save(order);
if (repository.findById(savedOrder.getId()).isPresent()) {
    // 实体处于"托管"状态
}

对托管实体调用仓库的delete()方法后,它转为"删除"状态。可通过删除后检查数据库是否不存在该实体来验证:

repository.delete(savedOrder);
if (!repository.findById(savedOrder.getId()).isPresent()) {
    // 实体处于"删除"状态
}

最后,当使用仓库的detach()方法使实体游离后,实体不再与持久化上下文关联。对游离实体的修改不会反映到数据库,除非明确合并回托管状态。可通过尝试修改游离实体来验证:

repository.detach(savedOrder);
// 修改实体
savedOrder.setName("New Order Name");

对游离实体调用save()会将其重新附加到持久化上下文,并在刷新持久化上下文时将变更持久化到数据库。

3. 使用Spring Data JPA保存实体

调用save()时,Spring Data JPA会将实体安排在事务提交时插入数据库。它会将实体添加到持久化上下文并标记为托管状态。

以下是在Spring Data JPA中使用save()方法持久化实体的简单代码片段:

Order order = new Order();
order.setCustomerName("John Doe");
order.setOrderDate(LocalDate.now());
Order savedOrder = repository.save(order);

⚠️ 重要提示:调用save()不会立即触发数据库插入操作。它仅将实体转为持久化上下文中的托管状态。因此,若其他事务在我们的事务提交前从数据库读取数据,可能会获取到不包含我们未提交变更的过期数据。

为确保数据保持最新,可采用两种方法:获取(Fetching)和刷新(Refreshing)。

4. Spring Data JPA中的实体获取

获取实体时,不会丢弃持久化上下文中对该实体的任何修改。我们仅从数据库检索实体数据,并将其添加到持久化上下文供后续处理

4.1. 使用findById()

Spring Data JPA仓库提供了findById()等便捷方法检索实体。这些方法始终从数据库获取最新数据,无论实体在持久化上下文中的状态如何。这种方法简化了实体检索,无需直接管理持久化上下文。

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

4.2. 急加载 vs 懒加载

急加载中,与主实体关联的所有相关实体会在检索主实体时同时从数据库获取。通过在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();

// 获取Order后直接访问OrderItems
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(); // 触发单独查询获取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;

// 假设order是托管实体
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) {
        // 刷新实体并可能重试更新
        entityManager.refresh(order);
        // 考虑添加处理重试或通知用户冲突的逻辑
    }
}

❌ 另需注意:若自上次检索后,被刷新的实体已被其他事务从数据库删除,refresh()可能抛出javax.persistence.EntityNotFoundException

6. 结论

本文我们学习了Spring Data JPA中刷新与获取实体的区别。获取涉及在需要时从数据库检索最新数据。刷新涉及用数据库中的最新数据更新持久化上下文中的实体状态。

通过策略性地运用这些方法,我们可以维护数据一致性,并确保所有事务都在最新数据上操作。

一如既往,示例源代码可在GitHub上获取。


原始标题:Refresh and Fetch an Entity After Save in JPA | Baeldung