位于上海,服务全国!

位于上海,服务全国!

如何通过JDBC API处理事务

作者:admin 分类: 时间:2017-03-02 14:48:15 点击量:491

有时我们想要一组相互关联的活动作为单个单元执行或完全不执行。 这个活动集合被称为事务。 一个事务必须作为组失败或成功,因为任务的各个单元中的中断可能对数据完整性的维护造成破坏。 在实际应用中,事务是如此重要,以至于它被保持在多个层或层次中。 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代码提供与数据库交互的手段。