在 Java 中提交 JdbcTemplate 或 DataSource
1. 概述
事務管理乍看之下很簡單,但一旦引入框架,就會變得複雜起來。當開發者使用純 JDBC 時,規則非常清晰。我們開啟連接,停用自動提交,執行 SQL 語句,然後根據結果明確地呼叫commit()或rollback() 。一切都清晰可見,盡在掌控之中。
然而,當引入 Spring 和JdbcTemplate時,這種思維模式就開始失效。許多有 JDBC 開發經驗的開發者會問一個很自然的問題:在使用JdbcTemplate或DataSource時,究竟該在哪裡呼叫 commit 方法?有些人嘗試手動取得連接,有些人嘗試將 Spring 抽象與底層 JDBC 呼叫混合使用,甚至有些人嘗試直接在DataSource上呼叫commit()方法。所有這些方法通常都會導致一些難以察覺的錯誤、意外回溯或資料不一致。
本教程旨在澄清這些疑惑。我們將探討 Spring 內部如何管理事務,為什麼手動提交無法如預期運作,以及如何使用宣告式和編製方法正確定義事務邊界。最後,我們將清楚地了解事務的所有者、提交發生的時間,以及如何僅在真正需要時才進行控制。
2. 理解核心困惑
2.1. 為什麼使用JdbcTemplate時commit()感覺不明確
造成困惑的根源在於JdbcTemplate工作方式。與普通的 JDBC 不同, JdbcTemplate不會直接向應用程式程式碼暴露Connection 。相反,它會從配置的DataSource請求連接,執行資料庫操作,然後將連接釋放回連接池。整個過程對開發人員來說是看不見的。
由於這種抽象,JdbcTemplate 中沒有可以呼叫commit的地方。開發者常常以為JdbcTemplate本身會提供一些與提交相關的 API,但實際上並沒有。這種設計是刻意為之的。 JdbcTemplate JdbcTemplate重點在於安全且有效率地執行 SQL,而不是事務控制。
當開發者試圖透過手動從DataSource取得Connection 、停用自動提交,然後將控制權交還給JdbcTemplate來繞過這個問題時,他們實際上是在與 Spring 的事務機制對抗。這通常會導致不可預測的行為,尤其是在涉及連接池或巢狀事務時。
2.2. 誰實際控制交易
在基於 Spring 的應用程式中,交易並非由 DAO、Repository 或 Service 類別擁有,而是由 Spring 容器本身擁有。 Spring 決定交易何時啟動、哪個連接參與**以及**何時提交或回滾。
為了更直觀地理解,請考慮以下邏輯流程:
這種職責分離是刻意為之的。它能確保業務邏輯保持清晰和專注,同時確保應用程式內事務處理的一致性。一旦了解這個概念,手動呼叫commit()函數就會顯得格格不入。
3. 使用@Transactional進行聲明式事務管理
3.1. 使用@Transactional定義邊界
在 Spring 中,管理事務最常用且建議的方法是使用@Transactional註解進行聲明式事務管理。開發者無需明確管理提交,只需聲明事務邊界,然後讓 Spring 處理事務生命週期即可。
在查看程式碼之前,請注意該方法內部沒有提交或回滾邏輯。業務邏輯保持簡潔易讀。
以下是一個簡單的服務範例,演示了這種模式:
@Service
public class OrderService {
private JdbcTemplate jdbcTemplate;
public OrderService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void placeOrder(long orderId, long productId) {
jdbcTemplate.update(
"insert into orders(id, product_id) values (?, ?)",
orderId,
productId
);
jdbcTemplate.update(
"update products set stock = stock - 1 where id = ?",
productId
);
}
}
Spring 會在placeOrder()執行之前啟動一個事務。兩個 SQL 語句都屬於同一個交易。如果方法正常完成,Spring 會自動提交交易。如果發生執行階段異常, Spring 會回溯交易。
3.2 事務管理器配置
聲明式事務需要事務管理器。在使用基於 JDBC 的存取時,通常會使用DataSourceTransactionManager 。
以下是一個基於 Java 的最小配置範例,用於實現事務管理:
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(
DataSource dataSource
) {
return new DataSourceTransactionManager(dataSource);
}
}
配置完成後,Spring 就能在遇到@Transactional註解時自動啟動、提交和回溯交易。此時,大多數應用程式都不需要手動處理事務提交。
3.3. 驗證自動提交
以下測試驗證了使用@Transactional時 Spring 會自動提交的說法。
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
@Transactional
void givenTransactionalMethod_whenNoException_thenTransactionCommits() {
orderService.placeOrder(1L, 100L);
}
}
此測試執行一個事務方法,無需明確提交。成功執行證實 Spring 已自動啟動並提交了事務。
由於測試本身是事務性的,Spring 會在測試完成後回滾更改,確保測試之間的隔離。
4. 具有明確提交控制的程序化事務管理
聲明式事務足以滿足大多數使用場景,但在某些情況下,開發人員必須以程式設計方式做出提交和回溯決策。 Spring 透過PlatformTransactionManager API 提供了支援。
這種方法的關鍵區別在於,除非應用程式明確要求,否則系統不會提交任何內容。
4.1. 程序化交易實施
在審查程式碼之前,理解其意圖至關重要。此程式碼庫明確啟動了一個事務,執行了多個資料庫操作,並且僅在收到指令時才提交。程式碼從未直接與 JDBC Connection互動或呼叫 ` commit()方法。
以下程式碼庫範例示範了這種方法:
@Repository
public class PaymentRepository {
private JdbcTemplate jdbcTemplate;
private PlatformTransactionManager transactionManager;
public PaymentRepository(
JdbcTemplate jdbcTemplate,
PlatformTransactionManager transactionManager
) {
this.jdbcTemplate = jdbcTemplate;
this.transactionManager = transactionManager;
}
public void processPayment(long paymentId, long amount) {
TransactionDefinition definition =
new DefaultTransactionDefinition();
TransactionStatus status =
transactionManager.getTransaction(definition);
jdbcTemplate.update(
"insert into payments(id, amount) values (?, ?)",
paymentId,
amount
);
jdbcTemplate.update(
"update accounts set balance = balance - ? where id = 1",
amount
);
transactionManager.commit(status);
}
}
在這裡,事務在 Spring 呼叫getTransaction()方法時開始。 Spring 只有在我們呼叫commit(status)時才會完成事務。
4.2. 驗證明確提交行為
以下測試驗證程序僅在明確呼叫commit()時才會持久化資料:
@SpringBootTest
class PaymentRepositoryTest {
@Autowired
private PaymentRepository paymentRepository;
@Test
void givenProgrammaticTransaction_whenCommitIsCalled_thenChangesArePersisted() {
paymentRepository.processPayment(1L, 200L);
}
}
此測試直接對應於程式碼倉庫的行為。由於呼叫了commit(status)方法,事務成功完成,變更被持久化。
如果移除 ` transactionManager.commit(status) ` 呼叫並再次執行測試,則不會持久化任何資料。這證實了程序化事務永遠不會自動提交。如果沒有明確的提交要求,Spring 會在方法完成後回溯交易。
5. 為什麼向DataSource提交請求是不正確的?
直接在 JDBC Connection上呼叫commit()會繞過 Spring 的交易同步機制。 Spring 可能正在協調一個涉及巢狀呼叫或多個資源的大型事務上下文。手動提交會破壞這種協調,導致資料部分持久化。
為了說明這種風險,我們考慮一個 REST 端點,它會對不同的服務執行多個資料庫操作。即使 HTTP 請求成功,在單一連線上手動呼叫commit()也不能保證整體一致性:
POST /payments
{
"paymentId": 1,
"amount": 200
}
如果一個服務手動提交而另一個服務提交失敗,系統最終會處於不一致的狀態。 Spring透過將所有提交和回滾操作路由到PlatformTransactionManager ,保證了**交易行為的可預測性和一致性**。
6. 結論
在使用JdbcTemplate或DataSource時,交易提交的位置最終取決於責任歸屬。在 Spring 應用中,事務責任由框架承擔,而非單一元件。
透過採用 Spring 的事務管理模型,我們可以編寫更簡潔的程式碼,避免難以察覺的錯誤,並確保應用程式行為的一致性。無論我們使用聲明式註解還是程式化控制,關鍵原則始終不變:永遠不要繞過 Spring 的事務管理器。
與往常一樣,這些範例的程式碼可以在 GitHub 上找到。