何時使用 Spring Data JPA 中的 getReferenceById() 和 findById() 方法
1. 概述
JpaRepository
為我們提供了CRUD運算的基本方法。然而,其中一些方法並不那麼簡單,有時很難確定哪種方法最適合特定情況。
getReferenceById(ID)
和findById(ID)
是經常造成此類混亂的方法。這些方法是getOne(ID), findOne(ID),
和getById(ID).
在本教程中,我們將了解它們之間的區別,並找出每種方法更適合的情況。
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
**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
此外,任何在此事務服務方法之外與此使用者互動的嘗試都會導致異常:
@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.3.具有新儲存庫事務的事務服務
讓我們來看一個更複雜的例子。想像一下,我們有一個儲存庫方法,每當我們呼叫它時,它都會創建一個單獨的事務:
@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));
}
我們可以使用幾種不同的傳播配置來創建事務之間更複雜的交互,並且它們可以產生不同的結果。
4。結論
findById()
和getReferenceById()
之間的主要區別在於它們何時將實體載入到持久性上下文中。了解這一點可能有助於實現最佳化並避免不必要的資料庫查找。這個過程與交易及其傳播緊密相關。這就是為什麼應該觀察交易之間的關係。
與往常一樣,本教程中使用的所有程式碼都可以在 GitHub 上找到。