文档

Java™ 教程
隐藏目录
使用事务
路径: JDBC数据库访问
课程: JDBC基础知识

使用事务

有时候你不希望一个语句在另一个语句完成之前生效。例如,当"The Coffee Break"的老板更新每周售出的咖啡量时,他还希望更新迄今为止的总销量。但是,每周销售量和总销售量应该同时更新;否则,数据将不一致。确保两个操作同时发生或都不发生的方法是使用事务。事务是一组一个或多个语句的集合,作为一个单位执行,所以要么所有语句都执行,要么所有语句都不执行。

本页面涵盖以下主题:

禁用自动提交模式

当创建连接时,它处于自动提交模式。这意味着每个单独的SQL语句被视为一个事务,并在执行后自动提交。(更准确地说,默认情况下,SQL语句在完成时被提交,而不是在执行时被提交。当所有结果集和更新计数都被检索完成时,语句被认为是完成的。然而,在几乎所有情况下,语句在执行后立即完成,因此被提交。)

将两个或多个语句分组到一个事务中的方法是禁用自动提交模式。下面的代码演示了如何禁用自动提交模式,其中con是一个活动连接:

con.setAutoCommit(false);

提交事务

在禁用自动提交模式后,直到显式调用commit方法之前,没有SQL语句被提交。在上一次调用commit方法之后执行的所有语句都包含在当前事务中,并作为一个单元一起提交。下面的方法CoffeesTable.updateCoffeeSales演示了一个事务,其中con是一个活动连接:

  public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))
    
    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("事务正在回滚");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

在这个方法中,连接con的自动提交模式被禁用,这意味着当方法commit被调用时,两个准备好的语句updateSalesupdateTotal将一起提交。无论是在自动提交模式启用时自动调用还是在禁用时显式调用,每次调用commit方法时,事务中语句的所有更改都将变为永久更改。在这种情况下,这意味着哥伦比亚咖啡的SALESTOTAL列已更改为50(如果TOTAL之前为0),并且将保持此值,直到使用另一个更新语句更改为止。

语句con.setAutoCommit(true);启用自动提交模式,这意味着每个语句在完成时将再次自动提交。然后,您又回到了默认状态,无需自己调用commit方法。建议仅在事务模式下禁用自动提交模式。这样,您可以避免为多个语句持有数据库锁,这增加了与其他用户发生冲突的可能性。

使用事务保护数据完整性

除了将语句组合在一起作为一个单元进行执行外,事务还可以帮助保护表中的数据完整性。例如,假设某个员工应该在表COFFEES中输入新的咖啡价格,但推迟了几天才这样做。与此同时,价格上涨了,今天店主正在输入较高的价格。员工最终开始输入现在已过时的价格,同时店主正在尝试更新表。在插入过时的价格后,员工意识到这些价格已不再有效,并调用Connection方法rollback来撤销其影响。(方法rollback中止事务并将值恢复为尝试更新之前的值。)与此同时,店主正在执行SELECT语句并打印新的价格。在这种情况下,店主有可能打印一个已回滚到其先前值的价格,从而使打印的价格不正确。

通过使用事务,可以避免这种情况,从而在两个用户同时访问数据时提供一定程度的冲突保护。

为了避免事务期间的冲突,数据库管理系统(DBMS)使用锁定机制,阻止其他人访问事务正在访问的数据。(请注意,在自动提交模式下,每个语句都是一个事务,锁只被保留一次语句。)设置锁定后,锁定将一直有效,直到事务提交或回滚。例如,DBMS可以锁定一个表的行,直到对其的更新被提交。这种锁定的效果是防止用户进行脏读,即在值被永久保存之前读取值。(访问尚未提交的更新值被认为是“脏读”,因为该值有可能被回滚到其之前的值。如果你读取了一个后来被回滚的值,你读取的是一个无效的值。)

锁定的设置方式由所谓的事务隔离级别决定,事务隔离级别可以从根本上不支持事务到支持强制执行非常严格的访问规则。

一个事务隔离级别的示例是TRANSACTION_READ_COMMITTED,它将不允许在提交之前访问一个值。换句话说,如果事务隔离级别被设置为TRANSACTION_READ_COMMITTED,DBMS不允许发生脏读。JDBC中的Connection接口包括五个值,表示可以在其中使用的事务隔离级别:

隔离级别 事务 脏读 不可重复读 幻读
TRANSACTION_NONE 不支持 不适用 不适用 不适用
TRANSACTION_READ_COMMITTED 支持 阻止 允许 允许
TRANSACTION_READ_UNCOMMITTED 支持 允许 允许 允许
TRANSACTION_REPEATABLE_READ 支持 阻止 阻止 允许
TRANSACTION_SERIALIZABLE 支持 阻止 阻止 阻止

当事务A检索一行数据,事务B随后更新该行数据,并且事务A之后再次检索相同的行数据时,就会发生不可重复读。事务A检索同一行数据两次,但是看到的数据却不同。

当事务A检索满足给定条件的一组行数据,事务B随后插入或更新一行数据,以使得该行数据现在也满足事务A的条件,并且事务A之后再次重复条件检索时,就会发生幻读。事务A现在看到了额外的一行数据,这行数据被称为幻行。

通常情况下,您不需要对事务隔离级别做任何操作;您可以直接使用数据库管理系统(DBMS)的默认隔离级别。默认事务隔离级别取决于您的DBMS。例如,对于Java DB,它是TRANSACTION_READ_COMMITTED。JDBC允许您查找您的DBMS设置的事务隔离级别(使用Connection方法getTransactionIsolation),并且还允许您将其设置为另一个级别(使用Connection方法setTransactionIsolation)。

注意:JDBC驱动程序可能不支持所有事务隔离级别。如果驱动程序不支持setTransactionIsolation调用中指定的隔离级别,驱动程序可以替换为更高、更严格的事务隔离级别。如果驱动程序无法替换为更高的事务级别,则抛出SQLException。使用方法DatabaseMetaData.supportsTransactionIsolationLevel来确定驱动程序是否支持给定级别。

设置和回滚到保存点

方法Connection.setSavepoint在当前事务中设置一个Savepoint对象。方法Connection.rollback重载以接受一个Savepoint参数。

以下方法CoffeesTable.modifyPricesByPercentage通过一个百分比priceModifier提高特定咖啡的价格。然而,如果新价格大于指定的价格maximumPrice,则将价格恢复为原始价格:

  public void modifyPricesByPercentage(
    String coffeeName,
    float priceModifier,
    float maximumPrice) throws SQLException {
    con.setAutoCommit(false);
    ResultSet rs = null;
    String priceQuery = "SELECT COF_NAME, PRICE FROM COFFEES " +
                        "WHERE COF_NAME = ?";
    String updateQuery = "UPDATE COFFEES SET PRICE = ? " +
                         "WHERE COF_NAME = ?";
    try (PreparedStatement getPrice = con.prepareStatement(priceQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
         PreparedStatement updatePrice = con.prepareStatement(updateQuery))
    {
      Savepoint save1 = con.setSavepoint();
      getPrice.setString(1, coffeeName);
      if (!getPrice.execute()) {
        System.out.println("无法找到名称为" + coffeeName + "的咖啡");
      } else {
        rs = getPrice.getResultSet();
        rs.first();
        float oldPrice = rs.getFloat("PRICE");
        float newPrice = oldPrice + (oldPrice * priceModifier);
        System.out.printf("咖啡%s的原价格为$%.2f%n", coffeeName, oldPrice);
        System.out.printf("咖啡%s的新价格为$%.2f%n", coffeeName, newPrice);
        System.out.println("正在执行更新操作...");
        updatePrice.setFloat(1, newPrice);
        updatePrice.setString(2, coffeeName);
        updatePrice.executeUpdate();
        System.out.println("\n更新后的COFFEES表:");
        CoffeesTable.viewTable(con);
        if (newPrice > maximumPrice) {
          System.out.printf("新价格$%.2f大于最高价格$%.2f," +
                            "正在回滚事务...%n",
                            newPrice, maximumPrice);
          con.rollback(save1);
          System.out.println("\n回滚后的COFFEES表:");
          CoffeesTable.viewTable(con);
        }
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    } finally {
      con.setAutoCommit(true);
    }
  }

以下语句指定当调用commit方法时,从getPrice查询生成的ResultSet对象的游标将被关闭。注意,如果您的DBMs不支持ResultSet.CLOSE_CURSORS_AT_COMMIT,则该常量将被忽略:

getPrice = con.prepareStatement(query, ResultSet.CLOSE_CURSORS_AT_COMMIT);

该方法通过以下语句创建一个Savepoint

Savepoint save1 = con.setSavepoint();

该方法检查新价格是否大于maximumPrice的值。如果是,则通过以下语句回滚事务:

con.rollback(save1);

因此,当该方法通过调用Connection.commit方法提交事务时,它不会提交任何已回滚的关联Savepoint的行;它将提交所有其他已更新的行。

释放保存点

Connection.releaseSavepoint 方法接受一个 Savepoint 对象作为参数,并从当前事务中移除它。

一旦保存点被释放,在回滚操作中引用它将会抛出 SQLException 异常。在事务提交或整个事务回滚时,所有创建的保存点会自动释放并失效。将事务回滚到一个保存点后,会自动释放并使无效在该保存点之后创建的其他保存点。

何时调用 rollback 方法

如前所述,调用 rollback 方法终止一个事务,并将修改的值返回到它们之前的状态。如果在事务中执行一个或多个语句时出现 SQLException,请调用 rollback 方法结束事务并重新开始事务。这是唯一的方法来知道什么已经提交和什么还没有提交。捕获 SQLException 只告诉您出现了问题,但不告诉您已经提交了什么或未提交什么。因为您不能确定没有提交任何内容,调用 rollback 方法是唯一确定的方法。

CoffeesTable.updateCoffeeSales 方法演示了一个事务,并包含一个调用 rollback 方法的 catch 块。如果应用程序继续并使用事务的结果,这个在 catch 块中调用 rollback 方法可以防止使用可能不正确的数据。


上一页: 使用 Prepared Statements
下一页: 使用 RowSet Objects