When to Use the getReferenceById() and findById() Methods in Spring Data JPA – 何时在 Spring Data JPA 中使用 getReferenceById() 和 findById() 方法

最后修改: 2024年 1月 1日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

JpaRepository provides us with basic methods for CRUD operations. However, some of them are not so straightforward, and sometimes, it’s hard to identify which method would be the best for a given situation.

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

getReferenceById(ID) and findById(ID) are the methods that often create such confusion. These methods are new API names for getOne(ID), findOne(ID), and getById(ID).

getReferenceById(ID)findById(ID) 这些方法是 getOne(ID)、findOne(ID) 和 getById(ID) 的新 API 名称。

In this tutorial, we’ll learn the difference between them and find the situation when each might be more suitable.

在本教程中,我们将了解它们之间的区别,并找出在哪些情况下它们可能更适合。

2. findById()

2. findById()

Let’s start with the simplest one out of these two methods. This method does what it says, and usually, developers don’t have any issues with it. It simply finds an entity in a repository given a specific ID:

让我们从这两种方法中最简单的一种开始。这种方法说到做到,开发人员通常不会有任何问题。它只需给定一个特定的 ID,就能在资源库中找到一个实体:

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

The method returns an Optional. Thus, assuming it would be empty if we passed a non-existent ID is correct.

该方法返回 选项因此,如果我们传递了一个不存在的 ID,假设它是空的是正确的。

The method uses eager loading under the hood, so we’ll send a request to our database whenever we call this method. Let’s check an example:

该方法在引擎盖下使用了急切加载功能,因此只要调用该方法,我们就会向数据库发送请求。让我们来看一个例子:

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

This method will generate the following logs:

此方法将生成以下日志:

[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 might batch requests in a transaction but will always execute them. Overall, findById(ID) doesn’t try to surprise us and does what we expect from it. However, the confusion arises because it has a counterpart that does something similar.

<总的来说,findById(ID) 并没有试图给我们带来惊喜,而是按照我们的预期来执行。然而,之所以会产生混淆,是因为它有一个做类似事情的对应程序。

3. getReferenceById()

3.getReferenceById()

This method has a similar signature to findById(ID):

该方法的签名与 findById(ID) 相似:</em

@Override
T getReferenceById(ID id);

Judging by the signature alone, we can assume that this method would throw an exception if the entity doesn’t exist. It’s true, but it’s not the only difference we have. The main difference between these methods is that getReferenceById(ID) is lazy. Spring won’t send a database request until we explicitly try to use the entity within a transaction.

仅从签名来看,我们可以认为如果实体不存在,该方法就会抛出异常。的确如此,但这并不是我们唯一的区别。这些方法之间的主要区别在于,getReferenceById(ID) 是懒惰的。在我们明确尝试在事务中使用实体之前,Spring 不会发送数据库请求。

3.1. Transactions

3.1 事务

Each transaction has a dedicated persistence context it works with. Sometimes, we can expand the persistence context outside the transaction scope, but it’s not common and useful only for specific scenarios. Let’s check how the persistence context behaves regarding the transactions:

每个事务都有一个专用的持久化上下文。有时,我们可以将持久化上下文扩展到事务范围之外,但这并不常见,而且只在特定场景中有用。让我们来看看持久化上下文在事务方面的表现:

Eager Loading With findById

Within a transaction, all the entities inside the persistence context have a direct representation in the database. This is a managed state. Thus, all the changes to the entity will be reflected in the database. Outside the transaction, the entity moved to a detached state, and changes won’t be reflected until the entity is moved back to the managed state.

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

Lazy-loaded entities behave slightly differently. Spring won’t load them until we explicitly use them in the persistence context:

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

Lazy Loading without usingSpring will allocate an empty proxy placeholder to fetch the entity from the database lazily. However, if we don’t do this, the entity will remain an empty proxy outside the transaction, and any call to it will result in a LazyInitializationException. However, if we do call or interact with the entity in the way it will require the internal information, the actual request to the database will be made:

Lazy Loading without usingSpring 将分配一个空的代理占位符,以便从数据库中懒散地获取实体。但是,如果我们不这样做,该实体将在事务之外保持空代理,并且对它的任何调用都将导致懒惰初始化异常但是,如果我们以需要内部信息的方式调用该实体或与之交互,就会向数据库发出实际请求:

Lazy Loading with using

3.2. Non-transactional Services

3.2.非事务服务

Knowing the behavior of transactions and the persistence context, let’s check the following non-transactional service, which calls the repository. The findUserReference doesn’t have a persistence context connected to it, and getReferenceById will be executed in a separate transaction:

了解了事务和持久化上下文的行为后,我们来检查一下下面这个调用存储库的非事务服务。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;
}

This code will generate the following log output:

该代码将生成以下日志输出:

[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

As we can see, there’s no database request. After understanding the lazy loading, Spring assumes that we might not need it if we don’t use the entity within. Technically, we cannot use it because our only transaction is one inside the getReferenceById method. Thus, the user we returned will be an empty proxy, which will result in an exception if we access its internals:

我们可以看到,没有数据库请求。在理解了懒加载后,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 Service

3.3.事务性服务

Let’s check the behavior if we’re using a @Transactional service:

让我们检查一下使用 @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;
}

This will give us a similar result for the same reason as in the previous example, as we don’t use the entity inside our transaction:

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

[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

Also, any attempts to interact with this user outside of this transactional service method would cause an exception:

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

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

However, now, the findUserReference method defines the scope of our transaction. This means that we can try to access the user in our service method, and it should cause the call to the database:

但是现在,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;
}

The code above would output the messages in the following order:

上述代码将按以下顺序输出信息:

[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

The request to the database wasn’t made when we called getReferenceById(), but when we called user.getFirstName(). 

当我们调用 getReferenceById() 时,并没有向数据库发出请求,而是在调用 user.getFirstName() 时。</em

3.3. Transactional Service With a New Repository Transaction

3.3.带有新存储库事务的事务性服务

Let’s check a bit more complex example. Imagine we have a repository method that creates a separate transaction whenever we call it:

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

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

Propagation.REQUIRES_NEW means the outer transaction won’t propagate, and the repository method will create its persistence context. In this case, even if we use a transactional service, Spring will create two separate persistence contexts that won’t interact, and any attempts to use the user will cause an exception:

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

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

We can use a couple of different propagation configurations to create more complex interactions between transactions, and they can yield different results.

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

3.4. Accessing Entities Without Fetching

3.4.无需获取即可访问实体

Let’s consider a real-life scenario. Imagine that we have a Group class:

让我们考虑一下现实生活中的场景。假设我们有一个 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
}

We want to add a user as an administrator to a group, and we can use either findById() or getReferenceById(). In this test, we get a user and make it an administrator of a new group using findById():

我们要将一个用户添加为一个组的管理员,可以使用 findById()getReferenceById() 。在此测试中,我们使用 findById() 获得一个用户并将其设为新组的管理员:</em

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

It would be reasonable to assume that we should have one SELECT query, but we’re getting two. This happens because of additional ORM checks. Let’s do a similar operation but use getReferenceById() instead:

按理说,我们应该只有一个 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);
}

In this scenario, we don’t need additional information about a user; we only need an ID. Thus, we can use a placeholder that getReferenceById() conveniently provides us, and we have a single INSERT without additional SELECTs.

在这种情况下,我们不需要用户的其他信息;我们只需要一个 ID。因此,我们可以使用 getReferenceById() 方便地提供给我们的占位符,而且我们只需一次 INSERT,无需额外的 SELECT。

This way, the database takes care of the correctness of the data while mapping. For example, we get an exception while using an incorrect ID:

这样,数据库在映射时就能确保数据的正确性。例如,在使用不正确的 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);
}

At the same time, we still have a single INSERT without any SELECTs.

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

However, we cannot use the same approach for adding users as group members. Because we use Set, the equals(T) and hashCode() methods will be called. Hibernate throws an exception as getReferenceById() doesn’t fetch a real object:

但是,我们不能使用同样的方法将用户添加为组成员。因为我们使用了 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);
      });
}

Thus, the decision about an approach should consider the data types and the context where we use the entity.

因此,在决定采用哪种方法时,应考虑数据类型和使用实体的环境。

4. Conclusion

4.结论

The main difference between findById() and getReferenceById() is when they load the entities into the persistence context. Understanding this might help to implement optimizations and avoid unnecessary database lookups. This process is tightly connected to the transactions and their propagation. That’s why the relationships between transactions should be observed.

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

As usual, all the code used in this tutorial is available over on GitHub.

与往常一样,本教程中使用的所有代码均可在 GitHub 上获取