1. 概述
JpaRepository
为我们提供了基础的 CRUD 操作方法。但其中一些方法并不那么直观,有时很难判断哪种方法最适合特定场景。
getReferenceById(ID)
和 findById(ID)
就是经常让人困惑的方法。 这些方法是 getOne(ID)
、findOne(ID)
和 getById(ID)
的新 API 名称。
在本教程中,我们将学习它们的区别,并找出各自更适合的场景。
2. findById()
先从这两个方法中最简单的一个开始。这个方法名副其实,开发者通常不会遇到问题。它只是根据给定的 ID 在仓库中查找实体:
@Override
Optional<T> findById(ID id);
方法返回一个 Optional
。因此,如果我们传入一个不存在的 ID,可以正确假设它会返回空。
该方法底层使用立即加载,所以每次调用时都会向数据库发送请求。看个例子:
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. 事务
每个事务都有其专用的持久化上下文。 有时我们可以将持久化上下文扩展到事务范围之外,但这并不常见,且仅适用于特定场景。让我们看看持久化上下文在事务中的行为:
在事务内,持久化上下文中的所有实体在数据库中都有直接表示。这是托管状态。因此,对实体的所有更改都会反映到数据库中。在事务之外,实体进入游离状态,更改不会反映到数据库,直到实体重新进入托管状态。
延迟加载的实体行为略有不同。Spring 不会加载它们,直到我们在持久化上下文中显式使用它们:
Spring 会分配一个空的代理占位符来延迟从数据库获取实体。但是,如果我们不这样做,实体在事务外将保持为空代理,任何调用都会导致
LazyInitializationException
。 但是,如果我们调用或以需要内部信息的方式与实体交互,就会向数据库发出实际请求:
3.2. 非事务性服务
了解了事务和持久化上下文的行为后,让我们检查以下调用仓库的非事务性服务。**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. 事务性服务
让我们检查使用 @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;
}
由于与上一个示例相同的原因(我们在事务内没有使用实体),这会产生类似的结果:
[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
此外,在此事务性服务方法之外与此 user 交互的任何尝试都会导致异常:
@Test
void whenFindUserReferenceUsingOutsideServiceThenThrowsException() {
User user = transactionalService.findUserReference(EXISTING_ID);
assertThatExceptionOfType(LazyInitializationException.class)
.isThrownBy(user::getFirstName);
}
但是,现在 findUserReference
方法定义了我们的事务范围。这意味着我们可以在服务方法中尝试访问 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.4. 带有新仓库事务的事务性服务
让我们看一个更复杂的例子。假设我们有一个仓库方法,每次调用时都会创建一个单独的事务:
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
User getReferenceById(Long id);
Propagation.REQUIRES_NEW
意味着外部事务不会传播,仓库方法将创建自己的持久化上下文。 在这种情况下,即使我们使用事务性服务,Spring 也会创建两个不会交互的独立持久化上下文,任何使用 user
的尝试都会导致异常:
@Test
void whenFindUserReferenceUsingInsideServiceThenThrowsExceptionDueToSeparateTransactions() {
assertThatExceptionOfType(LazyInitializationException.class)
.isThrownBy(() -> transactionalServiceWithNewTransactionRepository.findAndUseUserReference(EXISTING_ID));
}
我们可以使用几种不同的传播配置来创建事务之间更复杂的交互,它们可能会产生不同的结果。
3.5. 不获取实体的情况下访问实体
考虑一个实际场景。假设我们有一个 Group
类:
@Entity
@Table(name = "group")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne
private User administrator;
@OneToMany(mappedBy = "id")
private Set<User> users = new HashSet<>();
// getters, setters and other methods
}
我们想将一个用户添加为组的管理员,可以使用 findById()
或 getReferenceById()
。在这个测试中,我们使用 findById()
获取一个用户并将其设为新组的管理员:
@Test
void givenEmptyGroup_whenAssigningAdministratorWithFindBy_thenAdditionalLookupHappens() {
Optional<User> optionalUser = userRepository.findById(1L);
assertThat(optionalUser).isPresent();
User user = optionalUser.get();
Group group = new Group();
group.setAdministrator(user);
groupRepository.save(group);
assertSelectCount(2);
assertInsertCount(1);
}
合理假设应该只有一个 SELECT 查询,但我们得到了两个。这是因为额外的 ORM 检查。让我们执行类似操作但使用 getReferenceById()
:
@Test
void givenEmptyGroup_whenAssigningAdministratorWithGetByReference_thenNoAdditionalLookupHappens() {
User user = userRepository.getReferenceById(1L);
Group group = new Group();
group.setAdministrator(user);
groupRepository.save(group);
assertSelectCount(0);
assertInsertCount(1);
}
在这个场景中,我们不需要关于用户的额外信息;只需要一个 ID。因此,我们可以使用 getReferenceById()
方便提供的占位符,这样我们只有一个 INSERT 而没有额外的 SELECT。
这样,数据库在映射时负责数据的正确性。例如,使用错误的 ID 时会得到异常:
@Test
void givenEmptyGroup_whenAssigningIncorrectAdministratorWithGetByReference_thenErrorIsThrown() {
User user = userRepository.getReferenceById(-1L);
Group group = new Group();
group.setAdministrator(user);
assertThatExceptionOfType(DataIntegrityViolationException.class)
.isThrownBy(() -> {
groupRepository.save(group);
});
assertSelectCount(0);
assertInsertCount(1);
}
同时,我们仍然只有一个 INSERT 而没有任何 SELECT。
但是,我们不能使用相同的方法将用户添加为组成员。因为我们使用 Set
,会调用 equals(T)
和 hashCode()
方法。Hibernate 抛出异常,因为 getReferenceById()
没有获取真实对象:
@Test
void givenEmptyGroup_whenAddingUserWithGetByReference_thenTryToAccessInternalsAndThrowError() {
User user = userRepository.getReferenceById(1L);
Group group = new Group();
assertThatExceptionOfType(LazyInitializationException.class)
.isThrownBy(() -> {
group.addUser(user);
});
}
因此,选择方法时应考虑数据类型和使用实体的上下文。
4. 结论
findById()
和 getReferenceById()
的主要区别在于它们何时将实体加载到持久化上下文中。理解这一点有助于实现优化并避免不必要的数据库查找。这个过程与事务及其传播紧密相关。这就是为什么应该观察事务之间的关系。
如往常一样,本教程中使用的所有代码都可以在 GitHub 上找到。