Using Transactions

有时您不希望一个语句生效,除非另一个语句完成。例如,当“咖啡 Rest 时间”的所有者更新每周出售的咖啡量时,所有者也将希望更新迄今为止的销售总量。但是,每周的销售量和总销售量应同时更新;否则,数据将不一致。确保两个动作都发生或者两个动作都没有发生的方法是使用事务。事务是作为一个单元执行的一组一个或多个语句的集合,因此要么执行所有语句,要么不执行任何语句。

此页面涵盖以下主题

禁用自动提交 Pattern

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

允许将两个或多个语句组合到一个事务中的方法是禁用自动提交 Pattern。下面的代码对此进行了演示,其中con是活动连接:

con.setAutoCommit(false);

Committing Transactions

禁用自动提交 Pattern 后,除非明确调用方法commit,否则不会提交任何 SQL 语句。在上一次调用方法commit之后执行的所有语句都包含在当前事务中,并作为一个单元一起提交。以下方法CoffeesTable.updateCoffeeSales(其中con是活动连接)说明了事务:

public void updateCoffeeSales(HashMap<String, Integer> salesForWeek)
    throws SQLException {

    PreparedStatement updateSales = null;
    PreparedStatement updateTotal = null;

    String updateString =
        "update " + dbName + ".COFFEES " +
        "set SALES = ? where COF_NAME = ?";

    String updateStatement =
        "update " + dbName + ".COFFEES " +
        "set TOTAL = TOTAL + ? " +
        "where COF_NAME = ?";

    try {
        con.setAutoCommit(false);
        updateSales = con.prepareStatement(updateString);
        updateTotal = con.prepareStatement(updateStatement);

        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("Transaction is being rolled back");
                con.rollback();
            } catch(SQLException excep) {
                JDBCTutorialUtilities.printSQLException(excep);
            }
        }
    } finally {
        if (updateSales != null) {
            updateSales.close();
        }
        if (updateTotal != null) {
            updateTotal.close();
        }
        con.setAutoCommit(true);
    }
}

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

语句con.setAutoCommit(true);启用自动提交 Pattern,这意味着每个语句在完成后都会自动再次提交。然后,您将返回默认状态,您不必自己调用方法commit。建议仅在事务处理 Pattern 期间禁用自动提交 Pattern。这样,您可以避免为多个语句持有数据库锁,这会增加与其他用户发生冲突的可能性。

使用事务保留数据完整性

除了将语句分组在一起以作为一个单元执行之外,事务还可以帮助保留表中数据的完整性。例如,假设某名员工本应在表COFFEES中 Importing 新的咖啡价格,但延迟了几天。同时,价格上涨,如今所有者正在进入更高价格的过程。员工final在所有者try更新表格的同时四处 Importing 现在过时的价格。插入过时的价格后,员工意识到它们不再有效,并调用Connection方法rollback撤消其影响。 (方法rollback中止 Transaction,并将值恢复为try更新之前的值.)同时,所有者正在执行SELECT语句并打印新价格。在这种情况下,所有者可能会打印已经回滚到其先前值的价格,从而使打印的价格不正确。

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

为了避免事务期间的冲突,DBMS 使用锁,这是一种机制,用于阻止其他人对事务正在访问的数据的访问。 (请注意,在自动提交 Pattern 下,每个语句都是一个事务,仅对一个语句持有锁.)设置了锁之后,该锁将一直有效,直到提交或回滚该事务为止。例如,DBMS 可以锁定表的一行,直到已提交对该表的更新。此锁定的作用是防止用户获取脏读,即在将值设为永久值之前读取该值。 (访问尚未提交的更新值被视为“脏读”,因为该值可能会回滚到其先前的值.如果读取的值稍后被回滚,则您将读取无效的值值.)

锁的设置方式取决于所谓的事务隔离级别,该级别可以从完全不支持事务到支持强制执行非常严格的访问规则的事务。

事务隔离级别的一个示例是TRANSACTION_READ_COMMITTED,它只有在提交值之后才允许访问该值。换句话说,如果事务隔离级别设置为TRANSACTION_READ_COMMITTED,则 DBMS 不允许发生脏读。 Connectioninterface包含五个值,这些值代表您可以在 JDBC 中使用的事务隔离级别:

Isolation LevelTransactionsDirty ReadsNon-Repeatable ReadsPhantom Reads
TRANSACTION_NONENot supportedNot applicableNot applicableNot applicable
TRANSACTION_READ_COMMITTEDSupportedPreventedAllowedAllowed
TRANSACTION_READ_UNCOMMITTEDSupportedAllowedAllowedAllowed
TRANSACTION_REPEATABLE_READSupportedPreventedPreventedAllowed
TRANSACTION_SERIALIZABLESupportedPreventedPreventedPrevented

当事务 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);

    Statement getPrice = null;
    Statement updatePrice = null;
    ResultSet rs = null;
    String query =
        "SELECT COF_NAME, PRICE FROM COFFEES " +
        "WHERE COF_NAME = '" + coffeeName + "'";

    try {
        Savepoint save1 = con.setSavepoint();
        getPrice = con.createStatement(
                       ResultSet.TYPE_SCROLL_INSENSITIVE,
                       ResultSet.CONCUR_READ_ONLY);
        updatePrice = con.createStatement();

        if (!getPrice.execute(query)) {
            System.out.println(
                "Could not find entry " +
                "for coffee named " +
                coffeeName);
        } else {
            rs = getPrice.getResultSet();
            rs.first();
            float oldPrice = rs.getFloat("PRICE");
            float newPrice = oldPrice + (oldPrice * priceModifier);
            System.out.println(
                "Old price of " + coffeeName +
                " is " + oldPrice);

            System.out.println(
                "New price of " + coffeeName +
                " is " + newPrice);

            System.out.println(
                "Performing update...");

            updatePrice.executeUpdate(
                "UPDATE COFFEES SET PRICE = " +
                newPrice +
                " WHERE COF_NAME = '" +
                coffeeName + "'");

            System.out.println(
                "\nCOFFEES table after " +
                "update:");

            CoffeesTable.viewTable(con);

            if (newPrice > maximumPrice) {
                System.out.println(
                    "\nThe new price, " +
                    newPrice +
                    ", is greater than the " +
                    "maximum price, " +
                    maximumPrice +
                    ". Rolling back the " +
                    "transaction...");

                con.rollback(save1);

                System.out.println(
                    "\nCOFFEES table " +
                    "after rollback:");

                CoffeesTable.viewTable(con);
            }
            con.commit();
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (getPrice != null) { getPrice.close(); }
        if (updatePrice != null) {
            updatePrice.close();
        }
        con.setAutoCommit(true);
    }
}

以下语句指定在调用commit方法时关闭从getPrice查询生成的ResultSet对象的游标。请注意,如果您的 DBM 不支持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已回滚的任何行;它将提交所有其他更新的行。

Releasing Savepoints

方法Connection.releaseSavepointSavepoint对象作为参数并将其从当前事务中删除。

释放保存点后,try通过回滚操作引用该保存点将引发SQLException。提交事务或回滚整个事务时,将自动释放在事务中创建的所有保存点,并使其无效。将事务回滚到保存点会自动释放,并使在该保存点之后创建的任何其他保存点无效。

何时调用方法回滚

如前所述,调用方法rollback会终止事务并返回已修改为其先前值的所有值。如果您要在事务中执行一个或多个语句并获得SQLException,请调用rollback方法以结束事务并重新开始事务。这是知道已落实和未落实的唯一方法。catchSQLException会告诉您某些错误,但不会告诉您已提交或未提交的内容。因为您不能指望没有提交任何东西,所以确定方法rollback是唯一的确定方法。

方法CoffeesTable.updateCoffeeSales演示了一个事务,并包括一个调用方法rollbackcatch块。如果应用程序 continue 运行并使用事务结果,则对catch块中rollback方法的此调用将防止使用可能不正确的数据。