Mục lục
Transaction là một tập hợp các hoạt động đọc/ghi xuống database hoặc là chúng đều thực thi thành công hết hoặc không có hoạt động nào được thực thi xuống database.
Transaction đóng một vai trò rất quan trọng trong các ứng dụng thao tác với cơ sở dữ liệu. Nó đảm bảo các hoạt động cập nhật, xoá, lưu mới xuống database một cách đồng bộ.
JDBC transaction
Mặc định, JDBC connection sẽ bật mode auto-commit, do đó tất cả các câu lệnh SQL được thực thi ngay lập tức. Điều này sẽ ổn nếu ứng dụng của chúng ta chỉ ở mức đơn giản, thế nhưng nếu ứng dụng ngày một lớn hơn, thì ở đây có một số lý do mà chúng ta nên cân nhắc để tắt mode auto-commit để tự quản lý bằng mã code như để tăng hiệu xuất ứng dụng và đảm bảo tính toàn vẹn của dữ liệu.
Transaction cho phép chúng ta toàn quyền quản lý các thay đổi như thêm, sửa, xoá xuống database tại các thời điểm và điều kiện cụ thể. Trong transaction, mỗi câu lệnh SQL được xem như một đơn vị logic, nếu có bất kỳ mệnh đề nào không thực thi được hoặc bị lỗi thì tất cả các câu lệnh SQL đều không được cập nhật xuống database.
Để sử dụng và quản lý transaction cách thủ công thay vì sử dụng auto-commit mode, chúng ta sẽ sử dụng setAutoCommit(boolean) method để tắt auto-commit mode.
conn.setAutoCommit(false);
Commit và RollBack
Khi bạn đã hoàn tất các thay đổi của mình và bạn muốn thực hiện các thay đổi, hãy gọi phương thức commit() trên đối tượng kết nối như sau:
conn.commit( );
Nếu không, để khôi phục lại toàn bộ các thay đổi trong transaction đã thực hiện xuống database, chúng ta có thể sử dụng rollback method.
conn.rollback( );s
Điều kiện tiên quyết
Hầu hết các ứng dụng Java hiện đây đều sử dụng Maven hoặc Gradle cho phép chúng ta dễ dàng quản lý các dependency, build source code etc. Vì vậy trong bài viết này mình sẽ sử dụng project maven để hướng dẫn.
Sau khi tạo project maven thành công, chúng ta cần thêm JDBC dependency tương ứng với cơ sở dữ liệu bạn muốn, ở đây mình sử dụng mysql.
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> </dependency>
Tiếp theo, chúng ta sẽ cần tạo database dưới cơ sở dữ liệu MySQL theo lệnh sau:
CREATE DATABASE `empl`; CREATE TABLE `EMPLOYEE` ( `ID` int NOT NULL AUTO_INCREMENT, `NAME` varchar(45) DEFAULT NULL, `SALARY` decimal(10,0) DEFAULT NULL, PRIMARY KEY (`ID`) )
JDBC khi không có transaction
Trong phần này chúng ta sẽ tập trung làm rõ một ví dụ có làm mất tính nhất quán của dữ liệu.
Xem ví dụ dưới đây:
import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Main { private static final String DB_URL = "jdbc:mysql://localhost:3306/empl?serverTimezone=UTC&useSSL=false"; private static final String USER = "root"; private static final String PASS = "123456"; private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY) VALUES (?,?)"; private static final String SQL_UPDATE = "UPDATE EMPLOYEE SET SALARY=? WHERE NAME=?"; public static void main(String[] args) { try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT); PreparedStatement psUpdate = conn.prepareStatement(SQL_UPDATE)) { // them nhan vien join psInsert.setString(1, "join"); psInsert.setBigDecimal(2, new BigDecimal(10)); psInsert.execute(); // them nhan vien marry psInsert.setString(1, "marry"); psInsert.setBigDecimal(2, new BigDecimal(20)); psInsert.execute(); // cap nhat luong cho marry psUpdate.setBigDecimal(2, new BigDecimal(999.99)); psUpdate.setString(2, "marry"); psUpdate.execute(); } catch (Exception e) { e.printStackTrace(); } } }
Trong đó, mình thêm 2 nhân viên mới và sau đó cập nhật lương cho nhân viên có tên marry. Tuy nhiên, vì bất cẩn nên mình đã gán thông số sai cho câu lệnh update, dẫn đến lỗi sau
java.sql.SQLException: No value specified for parameter 1 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:964) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.PreparedStatement.checkAllParametersSet(PreparedStatement.java:2211) at com.mysql.jdbc.PreparedStatement.fillSendPacket(PreparedStatement.java:2191) at com.mysql.jdbc.PreparedStatement.fillSendPacket(PreparedStatement.java:2121) at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1162) at Main.main(Main.java:35)
Tuy nhiên khi truy vấn database thì 2 nhân viên được thêm mới đã được đồng bộ xuống database.
Đây quả thật là không như mong muốn đối với mình, vì mình mong muốn hoặc là tất cả các thay đổi phải được thực thi xuống database không thì rollback lại hết. Điều này có ý nghĩa rất thực thế khi bạn thiết kế một hàm xử lý business trong đó thực thi rất nhiều hoạt động với cơ sở dữ liệu, và chúng chỉ có ý nghĩa khi tất cả đều thực thi thành công.
JDBC transaction
Như đã đề cập cách sử dụng transaction trong JDBC, chúng ta chỉ cần sử dụng setAutoCommit(false) và gọi hàm commit() thủ công để thực thi các câu lệnh SQL xuống db.
import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Main { private static final String DB_URL = "jdbc:mysql://localhost:3306/empl?serverTimezone=UTC&useSSL=false"; private static final String USER = "root"; private static final String PASS = "123456"; private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY) VALUES (?,?)"; private static final String SQL_UPDATE = "UPDATE EMPLOYEE SET SALARY=? WHERE NAME=?"; public static void main(String[] args) { try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT); PreparedStatement psUpdate = conn.prepareStatement(SQL_UPDATE)) { conn.setAutoCommit(false); // them nhan vien join psInsert.setString(1, "deft"); psInsert.setBigDecimal(2, new BigDecimal(10)); psInsert.execute(); // them nhan vien marry psInsert.setString(1, "marrin"); psInsert.setBigDecimal(2, new BigDecimal(20)); psInsert.execute(); // cap nhat luong cho marry psUpdate.setBigDecimal(2, new BigDecimal(999.99)); psUpdate.setString(2, "deft"); psUpdate.execute(); conn.commit(); // nen tra lai default mode cho JDBC khi thuc thi xong conn.setAutoCommit(true); } catch (Exception e) { e.printStackTrace(); } } }
Trong đoạn code trên, lệnh commit được gọi sau khi tất cả các câu lệnh đã được thực thi, trong trường hợp lệnh update bị lỗi, các câu lệnh insert cũng sẽ không được thực thi.
import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Main { private static final String DB_URL = "jdbc:mysql://localhost:3306/empl?serverTimezone=UTC&useSSL=false"; private static final String USER = "root"; private static final String PASS = "123456"; private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY) VALUES (?,?)"; private static final String SQL_UPDATE = "UPDATE EMPLOYEE SET SALARY=? WHERE NAME=?"; public static void main(String[] args) { try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT); PreparedStatement psUpdate = conn.prepareStatement(SQL_UPDATE)) { conn.setAutoCommit(false); // them nhan vien join psInsert.setString(1, "deft"); psInsert.setBigDecimal(2, new BigDecimal(10)); psInsert.execute(); // them nhan vien marry psInsert.setString(1, "marrin"); psInsert.setBigDecimal(2, new BigDecimal(20)); psInsert.execute(); // cap nhat luong cho marry psUpdate.setBigDecimal(2, new BigDecimal(999.99)); psUpdate.setString(2, "deft"); psUpdate.execute(); conn.commit(); // nen tra lai default mode cho JDBC khi thuc thi xong conn.setAutoCommit(true); } catch (Exception e) { e.printStackTrace(); } } }
Output: 2 nhân viên tên deft và marrin đều không được thêm vào db.
Để sữa lỗi
java.sql.SQLException: No value specified for parameter 1
Của câu lệnh update chúng ta có thể sửa lại như sau:
// cap nhat luong cho marry psUpdate.setBigDecimal(1, new BigDecimal(99.99)); psUpdate.setString(2, "deft"); psUpdate.execute();
Mã code hoàn chỉnh
import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Main { private static final String DB_URL = "jdbc:mysql://localhost:3306/empl?serverTimezone=UTC&useSSL=false"; private static final String USER = "root"; private static final String PASS = "123456"; private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY) VALUES (?,?)"; private static final String SQL_UPDATE = "UPDATE EMPLOYEE SET SALARY=? WHERE NAME=?"; public static void main(String[] args) { try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT); PreparedStatement psUpdate = conn.prepareStatement(SQL_UPDATE)) { conn.setAutoCommit(false); // them nhan vien join psInsert.setString(1, "deft"); psInsert.setBigDecimal(2, new BigDecimal(10)); psInsert.execute(); // them nhan vien marry psInsert.setString(1, "marrin"); psInsert.setBigDecimal(2, new BigDecimal(20)); psInsert.execute(); // cap nhat luong cho marry psUpdate.setBigDecimal(1, new BigDecimal(99.99)); psUpdate.setString(2, "deft"); psUpdate.execute(); conn.commit(); // nen tra lai default mode cho JDBC khi thuc thi xong conn.setAutoCommit(true); } catch (Exception e) { e.printStackTrace(); } } }
Output
JDBC Savepoint
JDBC phiên bản 3.0 giới thiệu tính năng savepoint mới hỗ trợ quản lý transacion hiệu quả hơn. Hầu hết các DBMS hiện đại, hỗ trợ các điểm lưu trong môi trường của chúng như PL / SQL của Oracle.
Một savepoint là một điểm mà transaction sẽ rollback về đó nếu có lỗi xảy ra. Bạn có thể sử dụng rollback() method để trả lại tất cả các thay đổi được thực thi ngay sau một điểm savepoint.
Connection object hỗ trợ 2 method để chúng ta có thể triển khai savepoint:
- setSavepoint(String savepointName) – Định nghĩa một điểm savepoint mới.
- releaseSavepoint(Savepoint savepointName) – Xoá một savepoint đã được định nghĩa trước đó.
Và sử dụng rollback(String savepointName) để rollback lại các hoạt động thao tác vớii db được triển khai sau savepoint có tên savepointName.
import java.math.BigDecimal; import java.sql.*; public class Main { private static final String DB_URL = "jdbc:mysql://localhost:3306/empl?serverTimezone=UTC&useSSL=false"; private static final String USER = "root"; private static final String PASS = "123456"; private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY) VALUES (?,?)"; public static void main(String[] args) throws SQLException { Connection conn = null; Savepoint savepoint = null; try { conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT); conn.setAutoCommit(false); savepoint = conn.setSavepoint("Savepoint1"); psInsert.setString(1, "dn"); psInsert.setBigDecimal(2, new BigDecimal(10)); psInsert.execute(); psInsert.setString(1, "failed"); psInsert.setBigDecimal(3, new BigDecimal(10)); psInsert.execute(); conn.commit(); // nen tra lai default mode cho JDBC khi thuc thi xong conn.setAutoCommit(true); } catch (Exception e) { e.printStackTrace(); conn.rollback(savepoint); } } }
Nguồn