一、简介

使用JDBC API 创建的数据库连接具有称为自动提交模式的功能。

打开此模式可以帮助消除管理事务所需的样板代码 。然而,尽管如此,其目的以及在执行 SQL语句时如何影响事务处理有时仍不清楚。

在本文中,我们将讨论什么是自动提交模式以及如何正确使用它来进行自动和显式事务管理。我们还将介绍自动提交打开或关闭时要避免的各种问题。

2.什么是JDBC自动提交模式?

开发人员在使用 JDBC 时不一定了解如何有效地管理数据库事务。因此,如果手动处理事务,开发人员可能不会在适当的情况下启动它们或根本不启动它们。同样的问题也适用于在必要时发出提交回滚

为了解决这个问题, JDBC 中的自动提交模式提供了一种执行 SQL 语句的方法,其中事务管理由JDBC 驱动程序自动处理

因此,自动提交模式的目的是减轻开发人员自己管理事务的负担。这样,打开它可以更轻松地使用 JDBC API 开发应用程序。当然,这只适用于在每个 SQL 语句完成后立即保留数据更新的情况。

3. 自动提交为真时的自动事务管理

JDBC 驱动程序默认为新数据库连接打开自动提交模式。 当它打开时,它们会自动在自己的事务中运行每个单独的 SQL 语句。

除了使用此默认设置之外,我们还可以通过将 true 传递给连接的 setAutoCommit 方法来手动打开自动提交:

connection.setAutoCommit(true);

当我们之前关闭它,但后来我们需要恢复自动事务管理时,这种自行打开它的方法非常有用。

现在我们已经介绍了如何确保自动提交处于打开状态,接下来我们将演示如何设置自动提交,JDBC 驱动程序将在其自己的事务中运行 SQL 语句。也就是说,它们会立即自动将每个语句中的任何数据更新提交到数据库。

3.1.设置示例代码

在此示例中,我们将使用H2 内存数据库来存储数据。要使用它,我们首先需要定义 Maven 依赖项

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
</dependency>

首先,让我们创建一个数据库表来保存有关人员的详细信息:

CREATE TABLE Person (
    id INTEGER not null,
    name VARCHAR(50),
    lastName VARCHAR(50),
    age INTEGER,PRIMARY KEY (id)
)

接下来,我们将创建两个到数据库的连接。我们将使用第一个来运行 SQL 查询并更新表。我们将使用第二个连接来测试是否已对该表进行更新:

Connection connection1 = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
Connection connection2 = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");

请注意,我们需要使用单独的连接来测试已提交的数据。这是因为如果我们在第一个连接上运行任何选择查询,那么它们将看到尚未提交的更新。

现在我们将创建一个 POJO 来表示保存有关人员信息的数据库记录:

public class Person {

    private Integer id;
    private String name;
    private String lastName;
    private Integer age;

    // standard constructor, getters, and setters
}

要将记录插入到表中,我们创建一个名为 insertPerson 的方法:

private static int insertPerson(Connection connection, Person person) throws SQLException {    
    try (PreparedStatement preparedStatement = connection.prepareStatement(
      "INSERT INTO Person VALUES (?,?,?,?)")) {
        
        preparedStatement.setInt(1, person.getId());
        preparedStatement.setString(2, person.getName());
        preparedStatement.setString(3, person.getLastName());
        preparedStatement.setInt(4, person.getAge());
        
        return preparedStatement.executeUpdate();
    }        
}     

然后,我们将添加 updatePersonAgeById 方法来更新表中的特定记录:

private static void updatePersonAgeById(Connection connection, int id, int newAge) throws SQLException {
    try (PreparedStatement preparedStatement = connection.prepareStatement(
      "UPDATE Person SET age = ? WHERE id = ?")) {
        preparedStatement.setInt(1, newAge);
        preparedStatement.setInt(2, id);
        
        preparedStatement.executeUpdate();
    }
}

最后,我们添加一个 selectAllPeople 方法来选择表中的所有记录。我们将使用它来检查 SQL 插入更新 语句的结果:

private static List selectAllPeople(Connection connection) throws SQLException {
    
    List people = null;
    
    try (Statement statement = connection.createStatement()) {
        people = new ArrayList();
        ResultSet resultSet = statement.executeQuery("SELECT * FROM Person");

        while (resultSet.next()) {
            Person person = new Person();
            person.setId(resultSet.getInt("id"));
            person.setName(resultSet.getString("name"));
            person.setLastName(resultSet.getString("lastName"));
            person.setAge(resultSet.getInt("age"));
            
            people.add(person);
        }
    }
    
    return people;
}

现在有了这些实用方法,我们将测试打开自动提交的效果。

3.2.运行测试

为了测试我们的示例代码,我们首先将一个人插入表中。然后,从另一个连接,我们将检查数据库是否已更新,而无需发出 提交

Person person = new Person(1, "John", "Doe", 45);
insertPerson(connection1, person);

List people = selectAllPeople(connection2);
assertThat("person record inserted OK into empty table", people.size(), is(equalTo(1)));
Person personInserted = people.iterator().next();
assertThat("id correct", personInserted.getId(), is(equalTo(1)));

然后,将这条新记录插入到表中,让我们更新此人的年龄。此后,我们将从第二个连接检查更改是否已保存到数据库,而无需调用 commit

updatePersonAgeById(connection1, 1, 65);

people = selectAllPeople(connection2);
Person personUpdated = people.iterator().next();
assertThat("updated age correct", personUpdated.getAge(), is(equalTo(65)));

因此,我们通过测试验证了,当自动提交模式打开时,JDBC 驱动程序会在其自己的事务中隐式运行每个 SQL 语句。因此,我们不需要自己调用 commit 来持久更新数据库。

4. 自动提交错误时的显式事务管理

当我们想要自己处理事务并将多个SQL语句分组到一个事务中时,我们需要禁用自动提交模式。

我们通过将 false 传递给 连接的 setAutoCommit 方法来做到这一点:

connection.setAutoCommit(false);

当自动提交模式关闭时,我们需要通过在连接上调用 提交回滚 来手动标记每个事务的结束。

然而,我们需要注意的是,即使关闭自动提交,JDBC 驱动程序仍然会在需要时自动为我们启动事务。例如,这种情况发生在我们运行第一个 SQL 语句之前,以及每次提交或回滚之后。

让我们演示一下,当我们在关闭自动提交的情况下执行多个 SQL 语句时,只有在调用 commit 时,结果更新才会保存到数据库中。

4.1.运行测试

首先,我们使用第一个连接插入人员记录。然后,在不调用 commit 的 情况下,我们将断言我们无法从其他连接看到数据库中插入的记录:

Person person = new Person(1, "John", "Doe", 45);
insertPerson(connection1, person);

List<Person> people = selectAllPeople(connection2);
assertThat("No people have been inserted into database yet", people.size(), is(equalTo(0)));

接下来,我们将更新该记录中该人的年龄。和以前一样,我们将在不调用 commit 的情况下断言,我们仍然无法使用第二个连接从数据库中选择记录:

updatePersonAgeById(connection1, 1, 65);

people = selectAllPeople(connection2);
assertThat("No people have been inserted into database yet", people.size(), is(equalTo(0)));

为了完成我们的测试,我们将调用 commit 并断言我们现在可以使用第二个连接查看数据库中的所有更新:

connection1.commit();

people = selectAllPeople(connection2);
Person personUpdated = people.iterator().next();
assertThat("person's age updated to 65", personUpdated.getAge(), is(equalTo(65)));

正如我们在上面的测试中验证的那样,当自动提交模式关闭时,我们需要手动调用 commit 将更改保存到数据库。这样做将保存自当前事务开始以来我们执行的所有 SQL 语句的所有更新。如果这是我们的第一个事务,则要么是自我们打开连接以来,要么是在我们上次 提交回滚 之后。

5.注意事项和潜在问题

对于相对琐碎的应用程序,我们可以方便地运行打开自动提交的SQL语句。也就是说,不需要手动事务控制。然而,在更复杂的情况下,我们应该考虑到当 JDBC 驱动程序自动处理事务时,这有时可能会导致不需要的副作用或问题。

我们需要考虑的一件事是,当 自动提交开启时,可能会浪费大量的处理时间和资源 。这是因为它会导致驱动程序在其自己的事务中运行每个 SQL 语句(无论是否必要)。

例如,如果我们使用不同的值多次执行相同的语句,则每次调用都会包装在自己的事务中。因此,这可能会导致不必要的执行和资源管理开销。

因此,在这种情况下,我们通常最好关闭自动提交并将对同一 SQL 语句的多个调用显式批处理到一个事务中。这样做,我们可能会看到应用程序性能的显着提高。

另外需要注意的是,我们不建议在开放事务期间重新打开自动提交。这是因为 在事务中打开自动提交模式会将所有挂起的更新提交到数据库,无论当前事务是否完成 。因此,我们应该避免这样做,因为它可能会导致数据不一致。

六,结论

在本文中,我们讨论了 JDBC API 中自动提交模式的用途。我们还介绍了如何通过分别打开或关闭隐式和显式事务管理来启用它。最后,我们讨论了使用它时需要考虑的各种问题。

与往常一样,示例的完整源代码可以在 GitHub 上找到。