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)。有时,可以将持久化上下文扩展到事务范围之外,但这并不常见,只有在特定情况下才有用。
持久化上下文在事务中的表现如下:
在一个事务中,持久化上下文中的所有实体在数据库中都有直接的表示。这是一种受管(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