Spring Data JPA中的getReferenceById()和findById()方法

JpaRepository为我们提供了CRUD操作的基本方法。然而,其中一些方法并不那么简单,有时很难确定哪种方法最适合特定情况。

getReferenceById(ID)和findById(ID)是经常造成此类混乱的方法。这些方法是 getOne(ID)、findOne(ID)和getById(ID) 的新 API 名称。

getReferenceById()和findById()是两个可能在不同上下文中使用的方法:

  • getReferenceById():
    • getReferenceById(ID)是惰性延迟懒lazy方法。在我们明确尝试在事务中使用实体之前,Spring 不会发送数据库请求。
  • findById():
    • findById()通常是在数据库访问层或持久化层中使用的方法,用于根据给定的ID查找实体对象。在ORM(对象关系映射)框架中,这个方法通常用于根据数据库表的主键查找相应的对象。

事务
每个事务都有一个与之配合的专用持久性上下文。有时,我们可以将持久化上下文扩展到事务范围之外,但这并不常见,并且仅对特定场景有用。让我们检查一下持久化上下文在事务方面的行为方式:

  • 在事务内,持久性上下文内的所有实体在数据库中都有直接表示。这是一种托管managed状态。因此,对实体的所有更改都将反映在数据库中。
  • 在事务之外,实体移至分离detached状态,并且直到实体移回托管状态后才会反映更改。

延迟加载
延迟加载实体的行为略有不同。Spring 不会加载它们,直到我们在持久化上下文中明确使用它们:

Spring将分配一个空的代理占位符来延迟从数据库中获取实体。但是,如果我们不这样做,该实体将在事务之外仍然是一个空代理,并且对它的任何调用都将导致LazyInitializationException。 但是,如果我们确实以需要内部信息的方式调用实体或与实体交互,则将向数据库发出实际请求:

了解了事务的行为和持久化上下文后,让我们检查以下调用存储库的非事务服务。findUserReference没有连接到它的持久性上下文,并且getReferenceById 将 在单独的事务中执行:

public User findUserReference(long id) {
    log.info("Before requesting a user");
    User user = repository.getReferenceById(id);
    log.info(
"After requesting a user");
    return user;
}

这段代码没有数据库请求。

了解延迟加载之后,Spring 假设如果我们不使用其中的实体,我们可能不需要它。从技术上讲,我们无法使用它,因为我们唯一的事务是getReferenceById方法内的事务。因此,我们返回的用户将是一个空代理,如果我们访问其内部,这将导致异常:

public User findAndUseUserReference(long id) {
    User user = repository.getReferenceById(id);
    log.info("Before accessing a username");
    String firstName = user.getFirstName();
    log.info(
"This message shouldn't be displayed because of the thrown exception: {}", firstName);
    return user;
}

使用@Transactional服务时的行为:

@Transactional
public User findUserReference(long id) {
    log.info("Before requesting a user");
    User user = repository.getReferenceById(id);
    log.info(
"After requesting a user");
    return user;
}

任何在此事务服务方法之外与此用户交互的尝试都会导致异常:

@Test
void whenFindUserReferenceUsingOutsideServiceThenThrowsException() {
    User user = transactionalService.findUserReference(EXISTING_ID);
    assertThatExceptionOfType(LazyInitializationException.class)
      .isThrownBy(user::getFirstName);
}

但是在此事务服务内则可以,findUserReference方法定义了我们的事务范围。这意味着我们可以尝试在服务方法中访问用户,它应该会导致对数据库的调用:

@Transactional
public User findAndUseUserReference(long id) {
    User user = repository.getReferenceById(id);
    log.info("Before accessing a username");
    String firstName = user.getFirstName();
    log.info(
"After accessing a username: {}", firstName);
    return user;
}

但是,对数据库的请求不是在我们调用getReferenceById() 时发出的,而是在我们调用user.getFirstName() 时发出的。 

具有新存储库事务的事务服务
让我们看一个更复杂的例子。想象一下,我们有一个存储库方法,每当我们调用它时,它都会创建一个单独的事务:

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
User getReferenceById(Long id);

Propagation.REQUIRES_NEW意味着外部事务不会传播,并且存储库方法将创建其持久性上下文。在这种情况下,即使我们使用事务性服务,Spring也会创建两个独立的持久化上下文,它们不会交互,并且任何使用用户的尝试都会导致异常:

@Test
void whenFindUserReferenceUsingInsideServiceThenThrowsExceptionDueToSeparateTransactions() {
    assertThatExceptionOfType(LazyInitializationException.class)
      .isThrownBy(() -> transactionalServiceWithNewTransactionRepository.findAndUseUserReference(EXISTING_ID));
}

我们可以使用几种不同的传播配置来创建事务之间更复杂的交互,并且它们可以产生不同的结果。

结论
findById()和getReferenceById()之间的主要区别在于它们何时将实体加载到持久性上下文中。了解这一点可能有助于实现优化并避免不必要的数据库查找。这个过程与事务及其传播紧密相关。这就是为什么应该观察事务之间的关系。