如何通过JDBC API处理事务
有时我们想要一组相互关联的活动作为单个单元执行或完全不执行。 这个活动集合被称为事务。 一个事务必须作为组失败或成功,因为任务的各个单元中的中断可能对数据完整性的维护造成破坏。 在实际应用中,事务是如此重要,以至于它被保持在多个层或层次中。 Java库提供了API来实现这些功能。 JDBC API库是其中之一,它在这里与底层数据库系统紧密关联地被维护。 API支持是相当详尽的。 本文尝试介绍如何使用JDBC API处理事务。
事务中的潜在灾难
当一个应用程序想要在一个或多个表中进行任何更改时,至关重要的是所有更改都反映在数据库中,或者没有反映在数据库中。在这方面的一个典型例子是银行交易或预订机票。 假设某人想从一个帐户转钱到另一个帐户,即从当前帐户转到储蓄帐户。此外,假设两种不同类型的帐户实际上保存在不同的表中。将触发两个UPDATE语句以对两个不同的表进行适当的更改。 准确的余额金额由一个UPDATE语句减去,而另一个UPDATE语句将准确的金额添加到另一个帐户的余额。 实质上,一个更新是另一次更新的原因。很容易理解这两个更新语句必须作为一个单元出现。
下面以一个非常粗略的方式来表示一个简单的事物过程。
package transaction_demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionDemo {
private static final String URL =
"jdbc:mysql://localhost:3306/mybank";
private static final String USER = "root";
private static final String PASSWORD = "secret";
private static final String DRIVER =
"com.mysql.jdbc.Driver";
public void transferFund(String fromTable, String toTable,
String ofAccNumber, int amount)
throws ClassNotFoundException,
UnsufficientFundException, SQLException {
Class.forName(DRIVER);
try (Connection connection =
DriverManager.getConnection(URL, USER,
PASSWORD)) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement
.executeQuery("SELECT balance FROM "
+ fromTable + " WHERE acc_no ="
+ ofAccNumber);
resultSet.next();
int balance1 = resultSet.getInt(1) - amount;
if (balance1 < 0)
throw new
UnsufficientFundException("Unsufficient Fund.");
resultSet.close();
resultSet = statement.executeQuery("SELECT balance FROM "
+ toTable + " WHERE acc_no =" + ofAccNumber);
resultSet.next();
int balance2 = resultSet.getInt(1);
statement.executeUpdate(
"UPDATE " + fromTable + " SET balance=" + (balance1)
+ " WHERE acc_no=" + ofAccNumber);
statement.executeUpdate(
"UPDATE " + toTable + " SET balance="
+ (balance2 + amount)
+ " WHERE acc_no=" + ofAccNumber);
}
}
public void showBalance(String table, String accno)
throws ClassNotFoundException, SQLException {
Class.forName(DRIVER);
try (Connection connection =
DriverManager.getConnection(URL, USER,
PASSWORD)) {
Statement statement = connection.createStatement();
ResultSet resultSet =
statement.executeQuery("SELECT balance FROM "
+ table + " WHERE acc_no =" + accno);
while (resultSet.next())
System.out.println(table + " " + accno
+ " " + resultSet.getInt(1));
}
}
class UnsufficientFundException extends Exception {
private static final long serialVersionUID = 1L;
public UnsufficientFundException(String message) {
super(message);
}
}
public static void main(String[] args) throws Exception {
TransactionDemo t = new TransactionDemo();
t.showBalance("savings_acc", "1001");
t.showBalance("current_acc", "1001");
t.transferFund("savings_acc", "current_acc", "1001", 8000);
t.showBalance("savings_acc", "1001");
t.showBalance("current_acc", "1001");
}
}
代码运行,但是有一个潜在的事物问题,因为每个活动单位调用一个另类事件,没有应用机制来表示这些单独的活动作为一个单一的工作单元。 在执行期间,应用程序很可能在更新的交织过程之间被中断。 可能发生扣除的金额未反映在数据库中或金额从一个账户成功扣除,但这笔金额在转入的帐户中不会被更新,由于发生中断。 申请资金转移的人不知道扣除的金额在哪里。 这是混乱状态。 没有机制应用于代码来阻止这样的灾难。
因此,我们的第一个关注点是应用一些机制将活动分组为一个单元。 不幸的是,JDBC没有定义一个批量类活动的规则。 但是,其提供了一些方法,当应用时,使我们能够划分事务的开始和结束。
事务分界
JDBC提供的方法可用于定义事务的开始,事务的结束,并在事务结束时显式地执行提交或回滚。 提交指的是实际写入数据库表中的更改,并且回滚是指分别在任何给定时间点终止事务。
在JDBC中,并不总是需要显式地表示事务的开始,因为默认情况下所有更新都被认为是事务的一部分。 每次更新后,还会默认执行提交操作。 但是,我们可以通过调用具有true / false布尔值的Connection方法setAutoCommit()来禁用或启用此默认行为。
关于这个方法,commit()和rollback()分别用于结束或中止事务。
因为在给定时间点只有一个事务可以是活动的,所以只有一个连接可以是活动的。 如果我们想要有多个事务处于活动状态,我们还必须为每个事务获取多个连接。
现在,利用前面的知识,让我们重写早期的代码并纠正潜在的问题。
package transaction_demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionDemo {
private static final String URL =
"jdbc:mysql://localhost:3306/mybank";
private static final String USER = "root";
private static final String PASSWORD = "secret";
private static final String DRIVER =
"com.mysql.jdbc.Driver";
public void transferFund(String fromTable, String toTable,
String ofAccNumber, int amount)
throws ClassNotFoundException,
UnsufficientFundException, SQLException{
Class.forName(DRIVER);
Statement statement = null;
ResultSet resultSet = null;
Connection connection = null;
try {
connection = DriverManager.getConnection(URL, USER,
PASSWORD);
connection.setAutoCommit(false);
statement = connection.createStatement();
resultSet = statement
.executeQuery("SELECT balance FROM " + fromTable
+ " WHERE acc_no =" + ofAccNumber);
resultSet.next();
int balance1 = resultSet.getInt(1) - amount;
if (balance1 < 0)
throw new UnsufficientFundException("Unsufficient Fund.");
resultSet.close();
resultSet = statement.executeQuery("SELECT balance FROM "
+ toTable + " WHERE acc_no =" + ofAccNumber);
resultSet.next();
int balance2 = resultSet.getInt(1);
statement.executeUpdate(
"UPDATE " + fromTable + " SET balance=" + (balance1)
+ " WHERE acc_no=" + ofAccNumber);
statement.executeUpdate(
"UPDATE " + toTable + " SET balance="
+ (balance2 + amount)
+ " WHERE acc_no=" + ofAccNumber);
connection.commit();
}catch(SQLException ex){
connection.rollback();
throw ex;
}finally{
if (resultSet!= null)
resultSet.close();
if (statement != null)
statement.close();
connection.close();
}
}
public void showBalance(String table, String accno)
throws ClassNotFoundException, SQLException {
Class.forName(DRIVER);
try (Connection connection =
DriverManager.getConnection(URL, USER,
PASSWORD)) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT balance FROM "
+ table + " WHERE acc_no =" + accno);
while (resultSet.next())
System.out.println(table + " "
+ accno + " " + resultSet.getInt(1));
}
}
class UnsufficientFundException extends Exception {
private static final long serialVersionUID = 1L;
public UnsufficientFundException(String message) {
super(message);
}
}
public static void main(String[] args) throws Exception {
TransactionDemo t = new TransactionDemo();
t.showBalance("savings_acc", "1001");
t.showBalance("current_acc", "1001");
t.transferFund("savings_acc", "current_acc", "1001", 8000);
t.showBalance("savings_acc", "1001");
t.showBalance("current_acc", "1001");
}
}
保存点
可以随时调用rollback()方法,对实际修改数据库的事务进行中止。 JDBC 3.0引入了一个有趣的概念,称为保存点。 保存点允许我们指定可以回滚的事务子集,而不取消自事务开始以来所做的所有更改。 一个简单的类比来理解这是我们在玩游戏时所做的保存点。 我们可以从任何保存的位置开始播放。 保存点,以类似的方式,可以在第二次更新之前保存,并进行下一步。 在任何稍后的时间点,发生回滚,事务可以从表示的最后成功保存点开始。 这利用事务处理的效率,因为事务可以始终从最后定义的点开始,而不是从开始再次开始。 下面的代码片段可能会更好地对其进行说明。
Connection connection;
Savepoint savepoint = null;
// ...
try {
updateTable1(connection);
savepoint = connection.setSavepoint();
updateTable2(connection);
connection.commit();
}
catch (SQLException ex) {
if (savepoint != null) {
connection.rollback(savepoint);
}
else {
connection.rollback();
}
}
注意点
事务不仅限于UPDATE操作。 它们可以被应用于多个SELECT或INSERT语句或任何具有创建数据完整性问题的潜力CRUD组合。
由非托管事务引起的灾难具有特定的名称,例如脏读取,不可重复读取,幻影读取等。 任何关于数据库的标准书都会给出一个相当好的描述。
无论使用的SQL语句如何,实际的事务支持由所使用的底层数据库提供,而不是由JDBC驱动程序提供。 因此,如果保存点不在Java代码中运行,则将其归咎于数据库系统而不是JDBC API。
对于分布在多台机器上的表,事务会是怎样的情况? 其被称为分布式事务,这在本文中没有讨论。 进一步了解分布式事务的一些提示: Java通过JTA和JTS处理分布式事务,以及连接池。
结论
事务是一种作为单一操作单元执行的集体活动。JDBC提供了某些规则来以方法的形式来管理事务。这些API与底层数据库交互以产生实际效果。因此,事务实际上是数据库系统的力量,JDBC只是通过Java代码提供与数据库交互的手段。