これは、なにをしたくて書いたもの?
組み込み、インメモリーなデータベースとしてテストなどでよく使われるH2 Databaseですが、どういう利用形態があるかちゃんと見てきて
いなかったので、少し見ておきたいなということで。
自分はインメモリーで使うことが多いので、こちらに完全に寄せた使い方にします。ファイルを使った形態は扱いません。
H2 Databaseの利用形態
H2 DatabaseのWebサイトはこちら。
H2 Databaseへの接続形態は、以下の3つがあります。
- 組み込みモード
- サーバーモード
- 混合モード
組み込みがよく使われていそうな気がしますが、サーバーとしても利用できます。また、混合モードというものもあります。
データの保存先はメモリーとファイルから選べます。データを永続化したい場合は、ファイルに保存する必要があります。
データを永続化する必要がなかったり、より速度を求めたい場合にはメモリーを保存先にします。
Features / In-Memory Databases
各形態での接続URLはこちらに列挙されています。
Features / Database URL Overview
インメモリーを対象とした場合の接続URLの形式は以下があります。
jdbc:h2:mem:… データベース名がなく、(同じプロセスであっても)他のクライアントと同じデータベースを共有できないURLjdbc:h2:mem:<データベース名>… 同じプロセス内で、他のクライアントとデータベースを共有できるURLjdbc:h2:tcp://<ホスト名>:<ポート>/mem:<データベース名>… TCPを使ってネットワーク接続が可能なURL
最初の2つは組み込みモードになります。3つ目はサーバーモードですね。
なお、いずれも接続をクローズするとデータはなくなります。
接続をクローズしてもデータを保持するには、jdbc:h2:mem:test;DB_CLOSE_DELAY=-1のようにDB_CLOSE_DELAY=-1というパラメーターを
追加する必要があります。この場合、H2 Databaseが動作しているJavaVMの終了時にデータは破棄されることになります。
TCPで接続したい場合、サーバー用のクラスをなんらかの方法で実行する必要があります。サーバーの起動方法は、こちらに書かれています。
ちなみに保存先をファイルにした場合、最初に組み込みモードとしてアクセスした時にサーバーを起動することもできるようです。
これは混合モードというものみたいですね。この形態はインメモリーでは使えません。
Features / Automatic Mixed Mode
それから、ちょっと変わったものとしてサーブレットコンテナー向けにServletContextListenerが提供されていて、これを使ってServletContextに
接続を含めたり、サーバーを起動することもできるようです。
Tutorial / Using Databases in Web Applications
とまあ、このあたりを使ってH2 Databaseのインメモリー接続をいろいろと試してみたいと思います。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.3 2024-04-16 OpenJDK Runtime Environment (build 21.0.3+9-Ubuntu-1ubuntu122.04.1) OpenJDK 64-Bit Server VM (build 21.0.3+9-Ubuntu-1ubuntu122.04.1, mixed mode, sharing) $ mvn --version Apache Maven 3.9.8 (36645f6c9b5079805ea5009217e36f2cffd34256) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.3, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-112-generic", arch: "amd64", family: "unix"
準備
確認はプログラムで行うことにします。
Maven依存関係など。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.26.0</version> <scope>test</scope> </dependency> </dependencies>
テストコードで確認していきます。
組み込みモードで確認する
最初は組み込みモードでいろいろとバリエーションを確認してみましょう。まずはテストコードの雛形。
src/test/java/org/littlewings/h2/memory/H2InMemoryDatabaseTest.java
package org.littlewings.h2.memory; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.junit.jupiter.api.Test; import java.sql.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class H2InMemoryDatabaseTest { // ここにテストを書く }
データベース名のないプライベートなURL(jdbc:h2:mem:)で、接続間で同じデータを見れないことの確認。ここではひとつ接続を開いたまま
別に接続してもデータを共有できないことを確認しています。
@Test void inMemoryPrivate() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } }
脱線しますが、ユーザー名とパスワードを適当に指定しても通ります…。
@Test void withCredentials() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:", "user", "password")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } }
データベース名のないプライベートなURL(jdbc:h2:mem:)で、接続間で同じデータを見れないことの確認。こちらは最初の接続を先に
クローズしているというのが、先の例との違いになります。
@Test void shareInMemoryPrivate() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } }
次はデータベース名を指定するURL(jdbc:h2:mem:test_database)を使います。この場合、同じURLを指定した接続と、データを共有できることが
確認できます。
@Test void shareNamedDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } try (Connection conn3 = DriverManager.getConnection("jdbc:h2:mem:test_database_bar")) { assertThatThrownBy(() -> conn3.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } }
最後のひとつは、データベース名を変えたものですね。こうすると、同じデータは見えなくなります。
次は、データベース名を指定してはいるものの、接続を閉じた後に別の接続を開いた場合の確認です。この場合、接続を閉じた時点でデータは
失われるので最初の接続で作成したデータなどは見えなくなります。
@Test void closeDatabaseWhenConnectionClosed() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database2")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database2")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } }
接続をクローズした後もデータを共有したい場合は、DB_CLOSE_DELAY=-1をURLに追加します。
@Test void shareInMemoryDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database3;DB_CLOSE_DELAY=-1")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database3;DB_CLOSE_DELAY=-1")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } }
OKですね。
全体はこんな感じです。
src/test/java/org/littlewings/h2/memory/H2InMemoryDatabaseTest.java
package org.littlewings.h2.memory; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.junit.jupiter.api.Test; import java.sql.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class H2InMemoryDatabaseTest { @Test void inMemoryPrivate() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void withCredentials() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:", "user", "password")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void shareInMemoryPrivate() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareNamedDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } try (Connection conn3 = DriverManager.getConnection("jdbc:h2:mem:test_database_bar")) { assertThatThrownBy(() -> conn3.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } } @Test void closeDatabaseWhenConnectionClosed() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database2")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database2")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareInMemoryDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:mem:test_database3;DB_CLOSE_DELAY=-1")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:mem:test_database3;DB_CLOSE_DELAY=-1")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } }
サーバーモードで確認する
次は、サーバーモードで確認してみます。
H2 Databaseのディストリビューションをダウンロード。
$ curl -LO https://github.com/h2database/h2database/releases/download/version-2.2.224/h2-2023-09-17.zip $ unzip h2-2023-09-17.zip $ cd h2
ここからサーバーを起動します。-tcpでTCP接続可能なサーバーを起動します。
$ java -cp bin/h2-2.2.224.jar org.h2.tools.Server -tcp -ifNotExists TCP server running at tcp://127.0.1.1:9092 (only local connections)
デフォルトでは他のホストからのは接続できないので、OKとする場合は-tcpAllowOthersを指定します。
$ java -cp bin/h2-2.2.224.jar org.h2.tools.Server -tcp -tcpAllowOthers -ifNotExists TCP server running at tcp://127.0.1.1:9092 (others can connect)
ポートを指定する場合は、-tcpPortを指定します。デフォルトは9092ポートです。
$ java -cp bin/h2-2.2.224.jar org.h2.tools.Server -tcp -tcpPort 19092 -tcpAllowOthers -ifNotExists TCP server running at tcp://127.0.1.1:19092 (others can connect)
-ifNotExistsは、接続時にデータベースがなかった場合に自動作成するオプションです。これを指定しない場合、存在しないデータベースを
指定して接続するとこういうエラーをみることになります。
org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database "mem:test_database" not found, either pre-create it or allow remote database creation (not recommended in secure environments) [90149-224]
ちなみに、ダウンロードしたディストリビューションにはh2.shというスクリプトも含まれていて、ここからサーバーを起動することも
できるのですが。
$ sh bin/h2.sh -tcp -tcpAllowOthers TCP server running at tcp://127.0.1.1:9092 (others can connect)
オプションによっては指定できないようです。
$ sh bin/h2.sh -tcp -tcpAllowOthers -ifNotExists
null
Usage: java org.h2.tools.GUIConsole <options>
null
See also https://h2database.com/javadoc/org/h2/tools/GUIConsole.html
Exception in thread "main" org.h2.jdbc.JdbcSQLFeatureNotSupportedException: 機能はサポートされていません: "-ifNotExists"
Feature not supported: "-ifNotExists" [50100-224]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:568)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:475)
at org.h2.util.Tool.throwUnsupportedOption(Tool.java:70)
at org.h2.util.Tool.showUsageAndThrowUnsupportedOption(Tool.java:58)
at org.h2.tools.Console.runTool(Console.java:187)
at org.h2.tools.Console.main(Console.java:72)
なので、今回はServerクラスを直接指定して起動することにします。この使い方だったら、ローカルにダウンロードしたMavenアーティファクトを
指定して起動してもいいかもですね。
起動時に指定可能なオプションは、ServerクラスのJavadocに書かれています。
今回はこの指定で起動しておきます。
$ java -cp bin/h2-2.2.224.jar org.h2.tools.Server -tcp -tcpAllowOthers -ifNotExists TCP server running at tcp://127.0.1.1:9092 (others can connect)
このサーバーに接続するテストクラス。
内容は、先ほどのjdbc:h2:mem:といったJDBC URLに対して、h2とmemの間にtcp://<ホスト名>:<ポート番号>/が入り込む形式になったのが
変更点ですね。こういう感じ(jdbc:h2:tcp://localhost:9092/mem:)になります。mem:の前が/になっているのが注意点でしょうか。
JDBC URL以外は組み込みで動作させていた時と同じなので、説明は割愛します。
src/test/java/org/littlewings/h2/memory/H2ExternalProcessServerTest.java
package org.littlewings.h2.memory; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.junit.jupiter.api.Test; import java.sql.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class H2ExternalProcessServerTest { @Test void inMemoryPrivate() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void withCredentials() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:", "user", "password")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void shareInMemoryPrivate() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareNamedDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } try (Connection conn3 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database_bar")) { assertThatThrownBy(() -> conn3.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } } @Test void closeDatabaseWhenConnectionClosed() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database2")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database2")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareInMemoryDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } }
プログラム内でサーバーを起動する
プログラム内でサーバーを起動することもできます。以下のテストコードがその例です。
src/test/java/org/littlewings/h2/memory/H2InProcessServerTest.java
package org.littlewings.h2.memory; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.h2.tools.Server; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.sql.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class H2InProcessServerTest { private Server server; @BeforeEach void setUp() throws SQLException { server = Server.createTcpServer("-tcp", "-tcpAllowOthers", "-ifNotExists"); server.start(); } @AfterEach void tearDown() { server.stop(); } @Test void inMemoryPrivate() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void withCredentials() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:", "user", "password")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } @Test void shareInMemoryPrivate() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareNamedDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } try (Connection conn3 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database_bar")) { assertThatThrownBy(() -> conn3.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } } @Test void closeDatabaseWhenConnectionClosed() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database2")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database2")) { assertThatThrownBy(() -> conn2.prepareStatement("select id, name from t1 where id = ?")) .hasMessageContaining( "Table \"T1\" not found (this database is empty); SQL statement:\n" + "select id, name from t1 where id = ?" ).isInstanceOf( JdbcSQLSyntaxErrorException.class ); } } @Test void shareInMemoryDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } }
Server#createTcpServerでServerのインスタンスを作成します。引数は、コマンドラインと同じオプションが指定できるようです。
インスタンス作成時点ではサーバーは起動していないので、Server#startで開始、Server#stopで終了です。
private Server server; @BeforeEach void setUp() throws SQLException { server = Server.createTcpServer("-tcp", "-tcpAllowOthers", "-ifNotExists"); server.start(); } @AfterEach void tearDown() { server.stop(); }
テストの内容自体は先ほどの外部で起動したサーバーへのテストした内容と同じなので、説明は割愛します。
サーブレットコンテナにH2 Databaseサーバーを組み込む
最後は、ちょっと変わった形態です。サーブレットコンテナにH2 Databaseを組み込みます。
Tutorial / Using Databases in Web Applications
ServletContextListenerが提供されているので、これを使うとServletContextの中にH2 Databaseへの接続を含めたり、
Webアプリケーションの起動時にH2 Databaseのサーバーを起動できます。
なお、Jakarta EE 10向けにはJakartaDbStarterを、Java EE向けにはDbStarterを使います。
今回はJakarta EE 10(Jakarta Servlet 6.0)のApache Tomcat 10.1を使いましょう。
$ curl -LO https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.25/bin/apache-tomcat-10.1.25.tar.gz $ tar xf apache-tomcat-10.1.25.tar.gz $ cd apache-tomcat-10.1.25 $ rm -rf webapps/*
Maven依存関係。
<packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-bom</artifactId> <version>10.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> </dependencies> <build> <finalName>ROOT</finalName> </build>
web.xml。JakartaDbStarterをリスナーとして指定するのと、context-paramで設定を行えます。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <context-param> <param-name>db.url</param-name> <param-value>jdbc:h2:mem:servlet_context_database</param-value> </context-param> <context-param> <param-name>db.tcpServer</param-name> <param-value>-tcp -tcpAllowOthers -ifNotExists</param-value> </context-param> <listener> <listener-class>org.h2.server.web.JakartaDbStarter</listener-class> </listener> </web-app>
db.tcpServerは起動するサーバーの引数で、ここになにか指定しておくとサーバーを起動します。Server#startが実行される、ということになります。
db.urlはServletContextに含めるConnectionの接続内容です。
けっこう驚きの機能な気がしますが、こんな感じでServletContextからH2 Databaseへの接続を取得することができます。
src/main/java/org/littlewings/h2/memory/H2TestServlet.java
package org.littlewings.h2.memory; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @WebServlet("/h2") public class H2TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletContext servletContext = request.getServletContext(); Connection conn = (Connection) servletContext.getAttribute("connection"); try (PreparedStatement ps = conn.prepareStatement("select h2version()"); ResultSet rs = ps.executeQuery()) { rs.next(); String version = rs.getString(1); PrintWriter writer = response.getWriter(); writer.printf("Hello H2 %s!!", version); } catch (SQLException e) { throw new RuntimeException(e); } } }
パッケージングしてTomcatへデプロイ。
$ mvn package $ cp target/ROOT.war ../apache-tomcat-10.1.25/webapps
確認。ServletContextにH2 Databaseへの接続が含まれていることが確認できました。
$ curl localhost:8080/h2 Hello H2 2.2.224!!
今回の設定だと、サーバーも起動しています。
$ sudo ss -tnlp | grep 9092
LISTEN 0 50 *:9092 *:* users:(("java",pid=21695,fd=48))
なので、先ほどの外部で起動したH2 Databaseのサーバープロセス向けに書いたテストコードがそのまま実行できます。
src/test/java/org/littlewings/h2/memory/H2ExternalProcessServerTest.java
package org.littlewings.h2.memory; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.junit.jupiter.api.Test; import java.sql.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class H2ExternalProcessServerTest { @Test void inMemoryPrivate() throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:")) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn.commit(); try (PreparedStatement ps = conn.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } /* 省略 */ @Test void shareInMemoryDatabase() throws SQLException { try (Connection conn1 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { conn1.setAutoCommit(false); try (PreparedStatement ps = conn1.prepareStatement("create table t1(id int, name varchar(10), primary key(id))")) { ps.executeUpdate(); } try (PreparedStatement ps = conn1.prepareStatement("insert into t1(id, name) values(?, ?)")) { ps.setInt(1, 1); ps.setString(2, "H2Database"); ps.addBatch(); ps.clearParameters(); ps.setInt(1, 2); ps.setString(2, "MySQL"); ps.addBatch(); ps.clearParameters(); ps.executeBatch(); } conn1.commit(); } try (Connection conn2 = DriverManager.getConnection("jdbc:h2:tcp://localhost:9092/mem:test_database3;DB_CLOSE_DELAY=-1")) { try (PreparedStatement ps = conn2.prepareStatement("select id, name from t1 where id = ?")) { ps.setInt(1, 2); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.getString(2)).isEqualTo("MySQL"); } } } } } }
こんなところでしょうか。
おわりに
インメモリーで動作させているH2 Databaseに、いろいろな形態で接続して動作確認してみました。
今までなんとなく使っていて、あまり特性を把握していなかったこともあってよい勉強になりました。
基本的にはテストで利用することが多いのかなと思うのですが、このあたりをとっかかりにしてもう少しちゃんと使えるようになろうと思います。
データの保存先をファイルにする場合も、この延長になるのでドキュメントを見つつ、ですね。