Spring Data JPA 中的 Refresh 和 Fetch

1、简介

Java Persistence API(JPA)是连接 Java 对象和关系数据库的桥梁,允许我们无缝地持久化和检索数据。本文将会带你了解各种策略和技术,以便在 JPA 中进行保存操作后有效地刷新(Refresh)和获取(Fetch)实体。

2、了解 Spring Data JPA 中的实体管理

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

此外, Spring 容器会将实体管理器(EntityManager) 自动注入这些 repository 接口。该组件是嵌入 Spring Data JPA 的 JPA 基础架构的一个组成部分,可促进与底层持久化上下文(Persistence Context)的交互和 JPA 查询的执行。

2.1、Persistence Context

JPA 中的一个关键组件是持久化上下文(Persistence Context)。把这个上下文想象成一个临时存放区,JPA 在这里管理检索或创建实体的状态。

它可以确保:

  • 实体是唯一的: 在任何时候,上下文中都只存在一个具有特定主键的实体实例。
  • 跟踪更改: 实体管理器(EntityManager)会跟踪上下文中对实体属性所做的任何修改。
  • 保持数据一致性: 在事务处理期间,实体管理器会将上下文中的更改与底层数据库同步。

2.2、JPA 实体的生命周期

JPA 实体有四个不同的生命周期阶段: 瞬时(New)、托管(Managed)、删除(Removed)和游离(Detached)。

当我们使用实体的构造函数创建一个新实体实例时,它处于 “瞬时” 状态。我们可以通过检查实体的 ID(主键)是否为 null 来验证这一点:

Order order = new Order();
if (order.getId() == null) {
    // 实体处于“瞬时”状态
}

使用 repository 的 save() 方法持久化实体后,它就会过渡到 “托管” 状态。可以通过检查已保存的实体是否存在于 repository 中来验证这一点:

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

当对托管实体调用 repository 的 delete() 方法时,它就会过渡到 “删除” 状态。可以通过检查删除后的实体是否不再存在于数据库中来验证这一点:

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

最后,一旦使 repository 的 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.setName("New Order Name");

repository.save(order);

不过,需要注意的是,调用 save() 并不会立即触发数据库插入操作。相反,它只是将实体过渡到持久化上下文中的 “托管” 状态。因此,如果其他事务在我们的事务提交之前从数据库中读取数据,它们可能会检索到过时的数据,其中不包括我们所做但尚未提交的更改。

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

4、在 Spring Data JPA 中 Fetch 实体

fetch 实体时,我们不会丢弃在持久化上下文中对实体所做的任何修改。相反,我们只是从数据库中获取实体的数据,并将其添加到持久化上下文中进行进一步处理。

4.1、使用 findById()

Spring Data JPA Repository 提供了查找实体的便捷方法,如 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();

// 获取订单后直接访问订单项目
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(); // 这将触发一个单独的查询,以获取订单项目
    for (OrderItem item : items) {
        System.out.println("Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
    }
}

4.3、使用 JPQL

Java 持久性查询语言 (JPQL)允许我们针对实体而不是数据表写类似 SQL 的查询。它为根据各种条件检索特定数据或实体提供了灵活性。

来看一个按客户名称(customerName)和订单日期在指定范围内获取订单的示例:

@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 根据客户名称和订单日期范围等 Criteria 组合来获取订单:

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 Repository 并不提供内置的 refresh() 方法。

以下是使用 EntityManager 刷新实体的方法:

@Autowired
private EntityManager entityManager;

entityManager.refresh(order);

5.2、处理 OptimisticLockException

Spring Data JPA 中的 @Version 注解用于实现乐观锁。当多个事务试图并发更新同一个实体时,它有助于确保数据的一致性。当我们使用 @Version 时,JPA 会自动在我们的实体类上创建一个特殊字段(通常命名为 version)。

该字段存储一个 integer 值,代表数据库中实体的版本:

@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 中 Refresh 实体和 Fetch 实体之间的区别。Fetch 涉及在需要时从数据库中检索最新数据。Refresh 则是用数据库中的最新数据更新持久化上下文中实体的状态。


Ref:https://www.baeldung.com/spring-data-jpa-refresh-fetch-entity-after-save