原文地址:https://www.journaldev.com/2483/java-jdbc-transaction-management-savepoint
当我们处理关系数据库时,需要使用Java中的事务管理。我们使用JDBC API进行数据库操作,今天我们将学习如何使用JDBC事务管理。在《 JDBC教程》中,我们学习了如何使用JDBC API进行数据库连接并执行SQL查询。我们还研究了不同类型的驱动程序以及如何编写松散耦合的JDBC程序,这些程序可以帮助我们轻松地从一个数据库服务器切换到另一个数据库服务器。
Java JDBC中的事务管理
本教程旨在提供有关JDBC事务管理以及使用JDBC Savepoint进行部分回滚的详细信息。
默认情况下,当我们创建数据库连接时,它以自动提交模式运行。这意味着只要我们执行查询并完成查询,提交就会自动触发。因此,我们触发的每个SQL查询都是一个事务,如果我们正在运行一些DML或DDL查询,则在每个SQL语句完成后,所做的更改都会保存到数据库中。
有时,我们希望将一组SQL查询作为事务的一部分,以便在所有查询正常运行时提交它们。如果出现任何异常,我们可以选择回滚作为事务一部分执行的所有查询。
让我们通过一个简单的示例来了解一下,我们希望利用JDBC事务管理支持来确保数据完整性。假设我们有UserDB数据库,并且Employee信息保存到两个表中。对于我的示例,我正在使用MySQL数据库,但是它将在其他关系数据库以及Oracle和PostgreSQL上正常运行。
这些表将员工信息和地址详细信息存储在表中,这些表的DDL脚本如下所示。
CREATE TABLE `Employee` (
`empId` int(11) unsigned NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`empId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Address` (
`empId` int(11) unsigned NOT NULL,
`address` varchar(20) DEFAULT NULL,
`city` varchar(5) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL,
PRIMARY KEY (`empId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我们的最终项目如下图所示,我们将逐一研究每个类。
如您所见,在项目构建路径中有MySQL JDBC jar,因此我们可以连接到MySQL数据库。
DBConnection.java
package com.journaldev.jdbc.transaction;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBConnection {
public final static String DB_DRIVER_CLASS = "com.mysql.jdbc.Driver";
public final static String DB_URL = "jdbc:mysql://localhost:3306/UserDB";
public final static String DB_USERNAME = "pankaj";
public final static String DB_PASSWORD = "pankaj123";
public static Connection getConnection() throws ClassNotFoundException, SQLException {
Connection con = null;
// load the Driver Class
Class.forName(DB_DRIVER_CLASS);
// create the connection now
con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
System.out.println("DB Connection created successfully");
return con;
}
}
DBConnection是我们要创建供其他类使用的MySQL数据库连接的类。
EmployeeJDBCInsertExample.java
package com.journaldev.jdbc.transaction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class EmployeeJDBCInsertExample {
public static final String INSERT_EMPLOYEE_QUERY = "insert into Employee (empId, name) values (?,?)";
public static final String INSERT_ADDRESS_QUERY = "insert into Address (empId, address, city, country) values (?,?,?,?)";
public static void main(String[] args) {
Connection con = null;
try {
con = DBConnection.getConnection();
insertEmployeeData(con, 1, "Pankaj");
insertAddressData(con, 1, "Albany Dr", "San Jose", "USA");
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (con != null)
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void insertAddressData(Connection con, int id,
String address, String city, String country) throws SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_ADDRESS_QUERY);
stmt.setInt(1, id);
stmt.setString(2, address);
stmt.setString(3, city);
stmt.setString(4, country);
stmt.executeUpdate();
System.out.println("Address Data inserted successfully for ID=" + id);
stmt.close();
}
public static void insertEmployeeData(Connection con, int id, String name)
throws SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_EMPLOYEE_QUERY);
stmt.setInt(1, id);
stmt.setString(2, name);
stmt.executeUpdate();
System.out.println("Employee Data inserted successfully for ID=" + id);
stmt.close();
}
}
这是一个简单的JDBC程序,我们将在上面创建的Employee和Address表中插入用户提供的值。
现在,当我们运行该程序时,将得到以下输出。
DB Connection created successfully
Employee Data inserted successfully for ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'city' at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:23)
如您所见,当我们尝试将数据插入到地址表中时,由于该值大于列的大小,因此引发了SQLException。
如果查看Employee和Address表的内容,您会注意到Employee表中有数据,而Address表中没有数据。这将成为一个严重的问题,因为只有部分数据被正确插入,并且如果我们再次运行该程序,它将尝试再次插入Employee表并抛出以下异常。
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'PRIMARY'
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2941)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertEmployeeData(EmployeeJDBCInsertExample.java:57)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:21)
因此,我们现在无法为员工将数据保存到Address表中。因此,这个程序会导致数据完整性问题,这就是为什么我们需要事务管理来成功地插入两个表,或者在出现任何异常时回滚所有内容。
JDBC事务管理
JDBC API提供了setAutoCommit()方法,通过该方法我们可以禁用连接的自动提交功能。我们仅应在需要时禁用自动提交,因为除非我们在连接上调用commit()方法,否则不会提交事务。数据库服务器使用表锁来实现事务管理及其资源密集型过程。因此,我们应该在完成交易后立即提交交易。让我们编写另一个程序,在该程序中我们将使用JDBC事务管理功能来确保不违反数据完整性。
EmployeeJDBCTransactionExample.java
package com.journaldev.jdbc.transaction;
import java.sql.Connection;
import java.sql.SQLException;
public class EmployeeJDBCTransactionExample {
public static void main(String[] args) {
Connection con = null;
try {
con = DBConnection.getConnection();
//set auto commit to false
con.setAutoCommit(false);
EmployeeJDBCInsertExample.insertEmployeeData(con, 1, "Pankaj");
EmployeeJDBCInsertExample.insertAddressData(con, 1, "Albany Dr", "San Jose", "USA");
//now commit transaction
con.commit();
} catch (SQLException e) {
e.printStackTrace();
try {
con.rollback();
System.out.println("JDBC Transaction rolled back successfully");
} catch (SQLException e1) {
System.out.println("SQLException in rollback"+e.getMessage());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (con != null)
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
在运行该程序之前,请确保删除先前插入的数据。当您运行该程序时,将得到以下输出。
DB Connection created successfully
Employee Data inserted successfully for ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'city' at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
at com.journaldev.jdbc.transaction.EmployeeJDBCTransactionExample.main(EmployeeJDBCTransactionExample.java:19)
JDBC Transaction rolled back successfully
输出类似于先前的程序,但是如果您查看数据库表,则会注意到数据没有插入到Employee表中。现在,我们可以更改城市值,使其适合列,然后重新运行程序以将数据插入两个表中。请注意,只有在两个插入都执行良好且其中任何一个抛出异常时才提交连接,我们将回滚完整事务。
JDBC Savepoint
有时,一个事务可以是多个语句的组,我们希望回滚到事务中的特定点。JDBC Savepoint帮助我们在事务中创建检查点,我们可以回滚到那个特定的检查点。为事务创建的任何保存点都会自动释放,并且在提交事务或回滚整个事务时无效。将事务滚回保存点将自动释放并使在有问题的保存点之后创建的任何其他保存点无效。
假设我们有一个Logs表,我们要在其中记录成功保存员工信息的消息。但是,由于它仅用于记录日志,因此如果在插入“日志”表时出现任何异常,我们就不希望回滚整个事务。让我们看看如何通过JDBC保存点实现这一目标。
CREATE TABLE `Logs` (
`id` int(3) unsigned NOT NULL AUTO_INCREMENT,
`message` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
EmployeeJDBCSavePointExample.java
package com.journaldev.jdbc.transaction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;
public class EmployeeJDBCSavePointExample {
public static final String INSERT_LOGS_QUERY = "insert into Logs (message) values (?)";
public static void main(String[] args) {
Connection con = null;
Savepoint savepoint = null;
try {
con = DBConnection.getConnection();
// set auto commit to false
con.setAutoCommit(false);
EmployeeJDBCInsertExample.insertEmployeeData(con, 2, "Pankaj");
EmployeeJDBCInsertExample.insertAddressData(con, 2, "Albany Dr",
"SFO", "USA");
// if code reached here, means main work is done successfully
savepoint = con.setSavepoint("EmployeeSavePoint");
insertLogData(con, 2);
// now commit transaction
con.commit();
} catch (SQLException e) {
e.printStackTrace();
try {
if (savepoint == null) {
// SQLException occurred in saving into Employee or Address tables
con.rollback();
System.out
.println("JDBC Transaction rolled back successfully");
} else {
// exception occurred in inserting into Logs table
// we can ignore it by rollback to the savepoint
con.rollback(savepoint);
//lets commit now
con.commit();
}
} catch (SQLException e1) {
System.out.println("SQLException in rollback" + e.getMessage());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (con != null)
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
private static void insertLogData(Connection con, int i)
throws SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_LOGS_QUERY);
//message is very long, will throw SQLException
stmt.setString(1, "Employee information saved successfully for ID" + i);
stmt.executeUpdate();
System.out.println("Logs Data inserted successfully for ID=" + i);
stmt.close();
}
}
这个程序很容易理解。可以看到,在数据成功插入Employee和Address表之后,我正在创建保存点。如果出现了SQLException,并且savepoint为null,这意味着在执行针对Employee或Address表的insert查询时,会引发异常,因此我将回滚整个事务。
如果savepoint不为null,则意味着在将数据插入Logs表中时将出现SQLException,因此我仅将事务回滚到savepoint并提交。
如果您将运行上面的程序,则将看到以下输出。
DB Connection created successfully
Employee Data inserted successfully for ID=2
Address Data inserted successfully for ID=2
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'message' at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.insertLogData(EmployeeJDBCSavePointExample.java:73)
at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.main(EmployeeJDBCSavePointExample.java:30)
如果检查数据库表,您将注意到数据已成功插入Employee和Address表中。注意,当数据成功插入Employee和Address表并使用另一个事务插入日志表时,我们可以通过提交事务轻松地实现这一点。这只是一个展示在java程序中使用JDBC保存点的示例。