1. 序章

JDBC APIで作成されたデータベース接続には、自動コミットモードと呼ばれる機能があります。

このモードをオンにすると、トランザクションの管理に必要なボイラープレートコードを排除できます。 ただし、それにもかかわらず、その目的と、SQLステートメントを実行する際のトランザクション処理にどのように影響するかが不明確な場合があります。

この記事では、自動コミットモードとは何か、および自動および明示的なトランザクション管理の両方でそれを正しく使用する方法について説明します。 また、自動コミットがオンまたはオフの場合に回避するさまざまな問題についても説明します。

2. JDBC自動コミットモードとは何ですか?

開発者は、JDBCを使用するときにデータベーストランザクションを効果的に管理する方法を必ずしも理解していません。 したがって、トランザクションを手動で処理する場合、開発者は適切な場所でトランザクションを開始しないか、まったく開始しない可能性があります。 同じ問題が、必要に応じてcommitsまたはrollbacksの発行にも当てはまります。

この問題を回避するために、JDBCの自動コミットモードは、JDBCドライバーによって自動的に処理されるトランザクション管理を使用してSQLステートメントを実行する方法を提供します。

したがって、自動コミットモードの目的は、トランザクションを自分で管理する必要があるという開発者の負担を軽減することです。 このように、これをオンにすると、JDBCAPIを使用したアプリケーションの開発が容易になります。 もちろん、これは、各SQLステートメントが完了した直後にデータの更新が保持されることが許容される場合にのみ役立ちます。

3. 自動コミットが真の場合の自動トランザクション管理

JDBCドライバーは、デフォルトで新しいデータベース接続の自動コミットモードをオンにします。 オンの場合、個々のSQLステートメントを独自のトランザクション内で自動的に実行します。

このデフォルト設定を使用することに加えて、trueを接続のsetAutoCommitメソッドに渡すことにより、手動で自動コミットをオンにすることもできます。

connection.setAutoCommit(true);

この方法で自分でスイッチをオンにすると、以前にオフにしたときに便利ですが、後で自動トランザクション管理を復元する必要があります。

自動コミットがオンになっていることを確認する方法について説明したので、そのように設定すると、JDBCドライバーが独自のトランザクションでSQLステートメントを実行することを示します。 つまり、各ステートメントからのデータ更新をすぐにデータベースに自動的にコミットします。

3.1. サンプルコードの設定

この例では、H2インメモリデータベースを使用してデータを保存します。 これを使用するには、最初にMaven依存関係を定義する必要があります。

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

まず、人に関する詳細を保持するデータベーステーブルを作成しましょう。

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

次に、データベースへの2つの接続を作成します。 最初のものを使用して、テーブルでSQLクエリと更新を実行します。 そして、2番目の接続を使用して、そのテーブルが更新されているかどうかをテストします。

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

コミットされたデータをテストするには、別の接続を使用する必要があることに注意してください。 これは、最初の接続でselectクエリを実行すると、まだコミットされていない更新が表示されるためです。

次に、個人に関する情報を保持するデータベースレコードを表す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 insertおよびupdateステートメントの結果を確認します。

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. テストの実行

サンプルコードをテストするために、最初に人をテーブルに挿入しましょう。 その後、別の接続から、commitを発行せずにデータベースが更新されたことを確認します。

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)));

次に、この新しいレコードをテーブルに挿入して、その人の年齢を更新しましょう。 その後、2番目の接続から、 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. 自動コミットがFalseの場合の明示的なトランザクション管理

トランザクションを自分で処理し、複数のSQLステートメントを1つのトランザクションにグループ化する場合は、自動コミットモードを無効にする必要があります。

これを行うには、falseを接続のsetAutoCommitメソッドに渡します。

connection.setAutoCommit(false);

自動コミットモードがオフの場合、接続で commitまたはロールバックを呼び出して、各トランザクションの終了を手動でマークする必要があります。

ただし、自動コミットがオフになっている場合でも、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を呼び出さずに、2番目の接続を使用してデータベースからレコードを選択できないことを表明します。

updatePersonAgeById(connection1, 1, 65);

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

テストを完了するために、 commit を呼び出し、2番目の接続を使用してデータベース内のすべての更新を確認できることを表明します。

connection1.commit();

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

上記のテストで確認したように、自動コミットモードがオフの場合は、手動で commit を呼び出して、データベースへの変更を保持する必要があります。 そうすることで、現在のトランザクションの開始以降に実行したすべてのSQLステートメントからの更新が保存されます。 これは、これが最初のトランザクションである場合は接続を開いたため、または最後のcommitまたはrollbackの後で接続を開いたためです。

5. 考慮事項と潜在的な問題

比較的些細なアプリケーションでは、自動コミットをオンにしてSQLステートメントを実行すると便利な場合があります。 つまり、手動のトランザクション制御が必要ない場合です。 ただし、より複雑な状況では、JDBCドライバーがトランザクションを自動的に処理するときに、望ましくない副作用や問題が発生する可能性があることを考慮する必要があります。

考慮する必要があることの1つは、自動コミットがオンになっていると、処理時間とリソースを大量に浪費する可能性があることです。 これは、必要かどうかに関係なく、ドライバーが独自のトランザクションですべてのSQLステートメントを実行するためです。

たとえば、同じステートメントを異なる値で何度も実行すると、各呼び出しは独自のトランザクションにラップされます。 したがって、これは不要な実行とリソース管理のオーバーヘッドにつながる可能性があります。

したがって、このような場合は、通常、自動コミットをオフにして、同じSQLステートメントへの複数の呼び出しを1つのトランザクションに明示的にバッチ処理することをお勧めします。 そうすることで、アプリケーションのパフォーマンスが大幅に向上する可能性があります。

他に注意すべき点は、オープントランザクション中に自動コミットをオンに戻すことはお勧めできないということです。 これは、トランザクションの途中で自動コミットモードをオンにすると、現在のトランザクションが完了したかどうかに関係なく、保留中のすべての更新がデータベースにコミットされるためです。 したがって、データの不整合につながる可能性があるため、おそらくこれを避ける必要があります。

6. 結論

この記事では、JDBCAPIの自動コミットモードの目的について説明しました。 また、暗黙的および明示的なトランザクション管理をそれぞれオンまたはオフにすることで有効にする方法についても説明しました。 最後に、使用時に考慮すべきさまざまな問題や問題に触れました。

いつものように、例の完全なソースコードはGitHubにあります。