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

1、概览

JpaRepository 供了 CRUD 操作的基本方法。其中 getReferenceById(ID)findById(ID) 是经常引起混淆的方法。这些方法是 getOne(ID)findOne(ID)getById(ID) 的新 API 名称。

本文将带你了解这些方法之间的区别,以及各自的适用场景。

2、findById()

这个方法按照其名称所示,根据给定的 ID 在 Repository 中查找实体:

@Override
Optional<T> findById(ID id);

该方法返回一个 Optional。因此,如果传递了一个不存在的 ID,返回的 Optional 对象将为 empty

该方法使用了急切加载功能,因此只要调用该方法,就会向数据库发送请求。

执行如下示例:

public User findUser(long id) {
    log.info("Before requesting a user in a findUser method");
    Optional<User> optionalUser = repository.findById(id);
    log.info("After requesting a user in a findUser method");
    User user = optionalUser.orElse(null);
    log.info("After unwrapping an optional in a findUser method");
    return user;
}

输出的日志如下:

[2023-12-27 12:56:32,506]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.SimpleUserService - Before requesting a user in a findUser method
[2023-12-27 12:56:32,508]-[main] DEBUG org.hibernate.SQL - 
    select
        user0_."id" as id1_0_0_,
        user0_."first_name" as first_na2_0_0_,
        user0_."second_name" as second_n3_0_0_ 
    from
        "users" user0_ 
    where
        user0_."id"=?
[2023-12-27 12:56:32,508]-[main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
[2023-12-27 12:56:32,510]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.SimpleUserService - After requesting a user in a findUser method
[2023-12-27 12:56:32,510]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.SimpleUserService - After unwrapping an optional in a findUser method

Spring 可能会在事务中批量处理请求,但始终会执行这些请求。总的来说,findById(ID) 所做的正是我们所期望的。然而,令人困惑的是,它还有一个与之类似的功能。

3、getReferenceById()

该方法的签名与 findById(ID) 类似:

@Override
T getReferenceById(ID id);

仅凭签名就可以知道,如果实体不存在,这个方法会抛出异常。但这不是它们之间的唯一区别。这些方法之间的主要区别是 getReferenceById(ID) 方法是延迟加载的。在事务中显式尝试使用实体之前,Spring 不会发送数据库请求。

3.1、事务

每个事务都有一个专用的持久化上下文(Persistence Context)。有时,可以将持久化上下文扩展到事务范围之外,但这并不常见,只有在特定情况下才有用。

持久化上下文在事务中的表现如下:

Persistence Context

在一个事务中,持久化上下文中的所有实体在数据库中都有直接的表示。这是一种受管(managed)状态。因此,对实体的所有更改都会反映在数据库中。在事务之外,实体移动到了游离(detached)状态,在实体移动回受管(managed)状态之前,更改不会被反映出来。

懒加载实体的行为略有不同。在持久化上下文中明确使用实体之前,Spring 不会加载它们:

持久化上下文

Spring 将分配一个空的代理占位符,以便在需要时延迟从数据库中获取实体。如果不这样做,实体将在事务之外保持为空的代理状态,对其的任何调用都将导致 LazyInitializationException 异常。然而,如果以需要访问内部信息的方式调用或与实体交互,就会向数据库发起实际请求:

持久化上下文延迟加载实体

3.2、非事务性 Service

了解了事务的行为和持久化上下文后,来看看下面这个调用 Repository 的非事务 Service。

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;
}

输出日志如下:

[2023-12-27 13:21:27,590]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - Before requesting a user
[2023-12-27 13:21:27,590]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - After requesting a user

你可以看到,没有数据库请求。Spring 认为如果我们不使用其中的实体,可能就不需要它。从技术上讲,不能使用它,因为唯一的事务是 getReferenceById 方法中的事务。因此,返回的 user 将是一个空代理,如果访问它的内部结构,就会导致异常:

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;
}

3.3、事务性 Service

来看看使用 @Transactional Service 时的行为:

@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;
}

结果与上一个示例中的结果相似,因为没有在事务中使用实体:

[2023-12-27 13:32:44,486]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - Before requesting a user
[2023-12-27 13:32:44,486]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - After requesting a user

在此事务 Service 方法之外与该 user 交互的任何尝试都会导致异常:

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

现在,使用 @Transactional 注解为 findUserReference 方法定义了事务范围。这意味着可以尝试在 Service 方法中访问 user,而且应该会导致对数据库的调用:

@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;
}

执行上述代码,输出日志如下:

[2023-12-27 13:32:44,331]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - Before accessing a username
[2023-12-27 13:32:44,331]-[main] DEBUG org.hibernate.SQL - 
    select
        user0_."id" as id1_0_0_,
        user0_."first_name" as first_na2_0_0_,
        user0_."second_name" as second_n3_0_0_ 
    from
        "users" user0_ 
    where
        user0_."id"=?
[2023-12-27 13:32:44,331]-[main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
[2023-12-27 13:32:44,331]-[main] INFO  com.baeldung.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - After accessing a username: Saundra

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

3.3、具有新 Repository 事务的事务性 Service

来看一个更复杂的例子。假设有一个 Repository 方法,每次调用它都会创建一个单独的事务:

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

Propagation.REQUIRES_NEW 表示外部事务不会传播,Repository 方法将创建其持久化上下文。在这种情况下,即使使用事务性 Service,Spring 也会创建两个独立的持久化上下文,它们不会交互,任何使用 user 的尝试都会导致异常:

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

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

4、总结

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


Ref:https://www.baeldung.com/hibernate-queryexception-named-parameter-not-bound-fix