Trong bài viết trước, mình đã giới thiệu với các bạn về khái niệm transaction trong database, giúp chúng ta có thể đảm bảo dữ liệu luôn luôn được toàn vẹn và nhất quán. Spring cũng hỗ trợ cho chúng ta quản lý các transaction, giúp chúng ta chỉ tập trung vào phát triển các business logic mà không cần phải lo lắng nhiều về tính toàn vẹn của dữ liệu. Spring hỗ trợ nhiều loại transaction management nhưng trong bài viết này, mình chỉ giới thiệu với các bạn về JDBC transaction management trong Spring các bạn nhé!
Đầu tiên, mình sẽ tạo mới một Maven project để làm ví dụ:
Trong project này, mình khai báo Spring framework dependencies như sau: ngoài spring-context cho Spring Core, còn có spring-jdbc cho việc sử dụng JDBC API và spring-tx cho việc quản lý transaction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring-framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring-framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring-framework.version}</version> </dependency> |
với properties “spring-framework.version” là:
1 2 3 |
<properties> <spring-framework.version>6.1.10</spring-framework.version> </properties> |
Mình sẽ sử dụng JDBC với MySQL database, do đó mình cũng sẽ thêm MySQL driver vào project dependency của chúng ta.
1 2 3 4 5 |
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.0.0</version> </dependency> |
Database thì đã được định nghĩa với cấu trúc như sau:
1 2 3 4 5 |
CREATE TABLE account ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(45) NOT NULL, amount DECIMAL ); |
Ngoài ra chúng ta cần phải nói đến tập tin cấu hình của Spring, spring.xml, nằm trong thư mục /src/main/resources với nội dung ban đầu như sau:
1 2 3 4 5 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans> |
Trong ví dụ này, chúng ta sẽ tạo ra một ứng dụng ngân hàng nhỏ cho phép chúng ta có thể chuyển tiền từ tài khoản A sang tài khoản B,. Quá trình chuyển khoản này sẽ gồm 2 bước: trừ tiền trong tài khoản A và cộng tiền vào tài khoản B. Bất kỳ exception nào xảy ra trong quá trình chuyển tiền thì tất cả các hoạt động sẽ được rollback lại.
OK, bây giờ, mình sẽ đi hiện thực chi tiết từng class trong ví dụ của chúng ta nhé các bạn!
Đầu tiên, chúng ta phải nói đến đó là interface AccountDAO và hiện thực của nó AccountDAOImpl.
Những class này sẽ giúp chúng ta thao tác trực tiếp với database.
Nội dung của những class này như sau:
AccountDAO
1 2 3 4 5 6 7 8 9 10 |
package com.huongdanjava.springjdbctransactionmanagement.dao; import java.math.BigDecimal; public interface AccountDAO { BigDecimal getCurrentAmount(int id); void updateAmount(int id, BigDecimal amount); } |
Trong interface này, mình đã khai báo 2 methods, dùng để thao tác trực tiếp với database để lấy số tiền hiện tại trong một tài khoản và cập nhập số tiền cho một tài khoản.
AccountDAOImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.huongdanjava.springjdbctransactionmanagement.dao.impl; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.stereotype.Repository; import com.huongdanjava.springjdbctransactionmanagement.dao.AccountDAO; @Repository public class AccountDAOImpl implements AccountDAO { @Autowired private JdbcTemplate jdbcTemplate; public BigDecimal getCurrentAmount(int id) { String sql = "SELECT amount FROM account WHERE id=" + id; return jdbcTemplate.query(sql, new ResultSetExtractor<BigDecimal>() { public BigDecimal extractData(ResultSet resultSet) throws SQLException, DataAccessException { if (resultSet.next()) { return BigDecimal.valueOf(resultSet.getInt("amount")); } return BigDecimal.ZERO; } }); } public void updateAmount(int id, BigDecimal amount) { String sql = String.format("UPDATE account SET amount=%f WHERE id=%d", amount, id); jdbcTemplate.execute(sql); } } |
Class này thì mình đã khai báo nó trong Spring container sử dụng annotation @Repository và sử dụng đối tượng JdbcTemplate để thao tác với database.
Cái thứ hai đó chính là interface AccountService và hiện thực của nó AccountServiceImpl.
AccountService
1 2 3 4 5 6 7 8 |
package com.huongdanjava.springjdbctransactionmanagement.service; import java.math.BigDecimal; public interface AccountService { void transfer(int sourceId, int destId, BigDecimal amount) throws Exception; } |
Interface này thì chỉ có duy nhất một phương thức giúp chúng ta có thể chuyển tiền từ tài khoản này sang tài khoản khác.
AccountServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package com.huongdanjava.springjdbctransactionmanagement.service.impl; import java.math.BigDecimal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.huongdanjava.springjdbctransactionmanagement.dao.AccountDAO; import com.huongdanjava.springjdbctransactionmanagement.service.AccountService; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDAO accountDAO; @Transactional(rollbackFor = Exception.class) public void transfer(int sourceId, int destId, BigDecimal amount) throws Exception { BigDecimal sourceAmount = accountDAO.getCurrentAmount(sourceId); BigDecimal destAmount = accountDAO.getCurrentAmount(destId); // Update source account accountDAO.updateAmount(sourceId, sourceAmount.subtract(amount)); // Update dest account accountDAO.updateAmount(destId, destAmount.add(amount)); } } |
Đối với class này thì mình đã sử dụng @Service annotation để khai báo nó trong Spring container và sử dụng interface AccountDao để thao tác với database.
Cái nữa các bạn để ý là trên phương thức transfer(), mình có khai báo một annotation @Transactional với thuộc tính rollbackFor có giá trị là Exception.class. Annotation này, mục đích là để chỉ rõ method này sẽ được thực thi trong một transaction. Bất kỳ một thao tác nào bị lỗi trong method này, tất cả những thay đổi trước đó trong method này đều sẽ bị rollback.
Tiếp theo, chúng ta sẽ xem xét đến tập tin cấu hình của Spring.
Có một số khai báo mà chúng ta phải làm là:
– Enable auto component scan cho các class mà chúng ta đã khai báo sử dụng annotation @Repository và @Service.
1 |
<context:component-scan base-package="com.huongdanjava.springjdbctransactionmanagement"/> |
Bạn nào chưa rành về auto component scan thì có thể xem thêm ở bài viết này.
– Cấu hình Datasource để thao tác với database.
1 2 3 4 5 6 7 8 9 10 |
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/example"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <constructor-arg ref="dataSource"/> </bean> |
– Enable transaction management cho ứng dụng của chúng ta.
1 2 3 4 5 |
<tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> |
Trong khai báo trên, thẻ tx:annotation-driven dùng để khai báo với Spring rằng, chúng ta sẽ sử dụng annotation để quản lý transaction. Thuộc tính transaction-manager dùng để khai báo bean id của class sẽ quản lý transaction. Như các bạn thấy, ở đây chúng ta sử dụng đối tượng DataSourceTransactionManager để quản lý các transaction.
OK, vậy là chúng ta đã khai báo và hiện thực các class cần thiết. Bước cuối cùng, chúng ta sẽ hiện thực ứng dụng này.
Ví dụ bây giờ trong database của mình có 2 tài khoản A và B với các thông tin như sau:
Giờ mình sẽ viết code để thực hiện việc chuyển 10$ từ tài khoản A sang tài khoản B. Nội dung của class Application sẽ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.springjdbctransactionmanagement; import java.math.BigDecimal; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.huongdanjava.springjdbctransactionmanagement.service.AccountService; public class Application { public static void main(String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml"); AccountService as = (AccountService) ctx.getBean("accountServiceImpl"); as.transfer(1, 2, BigDecimal.valueOf(10)); } } |
Kết quả:
Đây là kết quả trong trường hợp mọi thứ đều OK.
Nếu mọi thứ không OK hết thì sao?
Giả sử bây giờ, sau khi đã trừ tiền trong tài khoản A, mình kiểm tra lại số tiền còn lại trong tài khoản A. Nếu số tiền dưới 20$ thì mình không cho chuyển nữa bằng cách throw một Exception như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.huongdanjava.springjdbctransactionmanagement.service.impl; import java.math.BigDecimal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.huongdanjava.springjdbctransactionmanagement.dao.AccountDAO; import com.huongdanjava.springjdbctransactionmanagement.service.AccountService; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDAO accountDAO; @Transactional(rollbackFor = Exception.class) public void transfer(int sourceId, int destId, BigDecimal amount) throws Exception { BigDecimal sourceAmount = accountDAO.getCurrentAmount(sourceId); BigDecimal destAmount = accountDAO.getCurrentAmount(destId); // Update source account BigDecimal subtract = sourceAmount.subtract(amount); accountDAO.updateAmount(sourceId, subtract); if (subtract.compareTo(BigDecimal.valueOf(20)) <= 0) { throw new Exception("Not allow send money if current amount is less than 20$"); } // Update dest account accountDAO.updateAmount(destId, destAmount.add(amount)); } } |
thì khi chạy với database như sau:
Một exception sẽ xảy ra:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Exception in thread "main" java.lang.Exception: Not allow send money if current amount is less than 20$ at com.huongdanjava.springjdbctransactionmanagement.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:26) at com.huongdanjava.springjdbctransactionmanagement.service.impl.AccountServiceImpl$$FastClassBySpringCGLIB$$9ca7be9f.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) at com.huongdanjava.springjdbctransactionmanagement.service.impl.AccountServiceImpl$$EnhancerBySpringCGLIB$$c260db33.transfer(<generated>) at com.huongdanjava.springjdbctransactionmanagement.Application.main(Application.java:15) |
và không có gì thay đổi trong database cả.
Minh
a ơi e code bị lỗi như này:
Exception in thread “main” org.springframework.dao.TransientDataAccessResourceException: StatementCallback; SQL [SELECT amount FROM account WHERE id=1Before start of result set; nested exception is java.sql.SQLException: Before start of result set
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:110)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1402)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:388)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:446)
at com.springjdbctransactionmanagement.dao.impl.AccountDaoImpl.getCurrentAmount(AccountDaoImpl.java:23)
at com.springjdbctransactionmanagement.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:18)
at com.springjdbctransactionmanagement.service.impl.AccountServiceImpl$$FastClassBySpringCGLIB$$b479d23b.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:747)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at com.springjdbctransactionmanagement.service.impl.AccountServiceImpl$$EnhancerBySpringCGLIB$$113f5fb.transfer()
at com.springjdbctransactionmanagement.Application.main(Application.java:18)
Caused by: java.sql.SQLException: Before start of result set
A giúp e với ạ
Khanh Nguyen
Khanh đã update code để fix cái lỗi này, bạn kiểm tra lại nhé!
Hải
Anh cho em hỏi, anh dùng gì để chạy mysql như hình thế ạ
Khanh Nguyen
Anh dùng Terminal trên macOS thôi em 😀
Hieu
Giả sử em chuyển tiền từ A sang B, bên A bị trừ tiền, kiểm tra dưới 20$ thì roll back, tức là nó không lưu vào db. Nhưng giờ em muốn lưu lịch sử bên A đã từng chuyển tiền mà bị lỗi thì làm như nào ạ ?
Hoang Hai
Trong class AccountServiceImpl có đối tượng AccountDAO dùng annotation @Autowired Nhưng trong spring.xml lại không có tag mà nó vẫn chạy được.
Em thấy nó chỉ là 1 interface vậy autowired sẽ nối nó với bean AccountDAOImpl có implements AccountDAO. Nhưng lỡ có nhiều hơn 1 class implements AccountDAO thì sao ạ.
Khanh Nguyen
Cái này anh đang dùng auto scan mà em! Em đọc thêm về https://huongdanjava.com/vi/su-dung-annotation-qualifier-trong-spring.html để hiểu cách làm nhé!
duong
Anh cho em hỏi của em bị lỗi này là sao ạ:
Exception in thread “main” org.springframework.jdbc.UncategorizedSQLException: StatementCallback; uncategorized SQLException for SQL [SELECT amount FROM account WHERE id=1]; SQL state [24000]; error code [0]; No current row in the ResultSet.; nested exception is java.sql.SQLException: No current row in the ResultSet.
Khanh Nguyen
Ko có data đó em
duong
vâng, em cảm ơn anh
Chuyen
Em chào anh. Anh cho em hỏi em nhận được lỗi như thế này khi chạy.
“Unable to load authentication plugin ‘caching_sha2_password'”
– Nếu em thêm ?useSSL=true sau value=”jdbc:mysql://localhost:3306/example?useSSL=true” hoặc
” ”
thì nó xuất hiện lỗi này ạ.
“PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target”
– Còn nếu em để như bên dưới thì lại sinh lỗi.
==> The reference to entity “verifyServerCertificate” must end with the ‘;’ delimiter.
Em nên cấu hình như thế nào ạ.
Em cảm ơn anh.
Khanh Nguyen
Em thử change ?useSSL=false xem em?
hoàng
cho mình hỏi vì sao khi tách đoạn code jdbc tách ra thành file mới thì ko sử dụng đc transaction nhỉ
Khanh Nguyen
Ví dụ như thế nào bạn? Khanh chưa hiểu ý của bạn lắm!
hoàng
đoạn code này :
mình tách ra thành file jdbc.xml
sau đó định nghĩa nó trong contex-param thì nó ko Transaction khi có Exception bạn ơi.
Khanh Nguyen
Không thấy code nhỉ?
Hoàng
vậy phân biệt cho em giữa transaction của spring và hibernate khác nhau điểm gì không ạ.tại sao em thấy có ví dụ sử dụng cả 2 ở trong impDAO vậy anh
X981
Rất hay và dễ hiểu.
Khanh Nguyen
Thanks bạn đã ủng hộ Hướng Dẫn Java!