何时在Spring Data JPA中使用getReferenceById()和findById()方法

2025/03/22

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,则假设它将为空,这是正确的。

该方法在底层使用了急切加载,因此每当我们调用此方法时,我们都会向数据库发送请求。让我们看一个例子:

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  cn.tuyucheng.taketoday.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  cn.tuyucheng.taketoday.spring.data.persistence.findvsget.service.SimpleUserService - After requesting a user in a findUser method
[2023-12-27 12:56:32,510]-[main] INFO  cn.tuyucheng.taketoday.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 非事务服务

了解了事务的行为和持久化上下文后,让我们检查以下调用Repository的非事务服务。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  cn.tuyucheng.taketoday.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - Before requesting a user
[2023-12-27 13:21:27,590]-[main] INFO  cn.tuyucheng.taketoday.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  cn.tuyucheng.taketoday.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - Before requesting a user
[2023-12-27 13:32:44,486]-[main] INFO  cn.tuyucheng.taketoday.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - After requesting a 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;
}

上面的代码将按以下顺序输出消息:

[2023-12-27 13:32:44,331]-[main] INFO  cn.tuyucheng.taketoday.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  cn.tuyucheng.taketoday.spring.data.persistence.findvsget.service.TransactionalUserReferenceService - After accessing a username: Saundra

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

3.4 具有新Repository事务的事务服务

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

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

Propagation.REQUIRES_NEW表示外部事务不会传播,并且Repository方法将创建其持久化上下文。在这种情况下,即使如果我们使用事务服务,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);
}

同时,我们仍然有一个没有任何SELECT的INSERT。

但是,我们不能使用相同的方法将用户添加为组成员。因为我们使用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()的主要区别是将实体加载到持久化上下文中的时间,了解这一点可能有助于实现优化并避免不必要的数据库查找,这个过程与事务及其传播紧密相关,这就是为什么应该观察事务之间的关系。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章