Java JDBC事务管理和保存点

时间:2020-02-23 14:34:29  来源:igfitidea点击:

当我们处理关系数据库时,需要使用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.theitroad.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 = "hyman";
	public final static String DB_PASSWORD = "hyman123";

	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.theitroad.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, "hyman");

			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.theitroad.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
	at com.theitroad.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.theitroad.jdbc.transaction.EmployeeJDBCInsertExample.insertEmployeeData(EmployeeJDBCInsertExample.java:57)
	at com.theitroad.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:21)

因此,现在我们无法将员工的数据保存到"地址"表中。
因此,该程序会导致数据完整性问题,这就是为什么我们需要事务管理才能成功地将它们插入到两个表中,或者在发生任何异常时回滚所有内容。

JDBC事务管理

JDBC API提供了setAutoCommit()方法,通过该方法可以禁用连接的自动提交功能。
我们仅应在需要时禁用自动提交,因为除非我们在连接上调用commit()方法,否则不会提交事务。
数据库服务器使用表锁来实现事务管理及其资源密集型过程。
因此,我们应该在完成交易后立即提交交易。
让我们编写另一个程序,在该程序中我们将使用JDBC事务管理功能来确保不违反数据完整性。

EmployeeJDBCTransactionExample.java

package com.theitroad.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, "hyman");

			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.theitroad.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
	at com.theitroad.jdbc.transaction.EmployeeJDBCTransactionExample.main(EmployeeJDBCTransactionExample.java:19)
JDBC Transaction rolled back successfully

输出类似于先前的程序,但是如果您查看数据库表,则会注意到数据没有插入Employee表。
现在我们可以更改城市值,使其适合列,然后重新运行程序以将数据插入两个表中。
请注意,只有在两个插入都执行良好且其中任何一个抛出异常时才提交连接,我们将回滚完整事务。

JDBC保存点

有时一个事务可以是多个语句组成的组,我们想回滚到事务中的特定点。
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.theitroad.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, "hyman");

			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表执行插入查询时引发了异常,因此我回滚了完整的事务。

如果savepoint不为null,则意味着将SQLException插入到Logs表中,因此我仅将事务回滚到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.theitroad.jdbc.transaction.EmployeeJDBCSavePointExample.insertLogData(EmployeeJDBCSavePointExample.java:73)
	at com.theitroad.jdbc.transaction.EmployeeJDBCSavePointExample.main(EmployeeJDBCSavePointExample.java:30)

如果您要检查数据库表,则会注意到该数据已成功插入Employee和Address表中。
请注意,通过将数据成功插入Employee和Address表中并使用另一个事务插入日志表中来提交事务,我们可以轻松实现此目的。