現代の世界では、データ ストレージが欠かせません。データベースの操作の歴史は、JDBC の出現からずっと昔に始まりました。JDBC 上に構築された最新のフレームワークに欠かせないものを覚えておくことをお勧めします。また、一緒に仕事をする上でも、時には「原点回帰」する機会も必要かもしれません。このレビューが入門として、あるいは記憶を新たにするのに役立つことを願っています。
この標準は、「 JSR 221 JDBC 4.1 API 」仕様で説明されています。この仕様は、JDBC API が Java で書かれたプログラムからリレーショナル データベースへのプログラムによるアクセスを提供することを示しています。また、JDBC API は Java プラットフォームの一部であるため、Java SE および Java EE に含まれていることもわかります。JDBC API は、java.sql と javax.sql の 2 つのパッケージで提供されます。それでは彼らについて知りましょう。
ご覧のとおり、これは論理的ですが、データベースは Java SE にネイティブではない外部コンポーネントです。これを簡単に説明すると、膨大な数のデータベースがあり、どのデータベースでも作業できるということです。たとえば、PostgreSQL、Oracle、MySQL、H2 などがあります。これらの各データベースは、データベース ベンダーと呼ばれる別の会社によって提供されます。各データベースは、独自のプログラミング言語 (必ずしも Java である必要はありません) で作成されます。Java アプリケーションからデータベースを操作できるようにするために、データベース プロバイダーは独自のイメージ アダプターである特別なドライバーを作成します。このような JDBC 互換のもの(つまり、JDBC ドライバーを備えたもの)は、「JDBC 準拠データベース」とも呼ばれます。ここで、コンピューターデバイスに例えることができます。たとえば、メモ帳には「印刷」ボタンがあります。これを押すたびに、プログラムはオペレーティング システムにメモ帳アプリケーションが印刷を要求していることを伝えます。そしてプリンターをお持ちですね。オペレーティング システムに Canon または HP プリンタと均一に通信できるようにするには、さまざまなドライバが必要になります。しかし、ユーザーであるあなたにとっては何も変わりません。それでも同じボタンを押すことになります。JDBCも同様です。同じコードを実行していますが、内部では異なるデータベースが実行されている可能性があるだけです。これは非常に明確なアプローチだと思います。このような各 JDBC ドライバーは、ある種のアーティファクト、ライブラリ、jar ファイルです。これは私たちのプロジェクトの依存関係です。たとえば、データベース「H2 Database」を選択し、次のように依存関係を追加する必要があります。
導入
プログラミング言語の主な目的の 1 つは、情報の保存と処理です。データ ストレージがどのように機能するかをより深く理解するには、アプリケーションの理論とアーキテクチャに少し時間を費やす価値があります。たとえば、Joseph Ingeno 著「ソフトウェア アーキテクトのハンドブック: 効果的なアーキテクチャを実装してソフトウェア アーキテクトとして成功する」という文献を読むことができます。前述したように、特定のデータ層または「データ層」が存在します。これには、データを保存する場所 (SQL データベースなど) とデータ ストアを操作するためのツール (後述する JDBC など) が含まれます。Microsoft の Web サイトには、「インフラストラクチャ永続層の設計」という記事もあります。この記事では、データ層から追加の層である永続層を分離するアーキテクチャ ソリューションについて説明しています。この場合、データ層はデータ自体のストレージのレベルであり、永続層はデータ層レベルからストレージのデータを操作するための抽象化レベルです。永続層には、「DAO」テンプレートまたはさまざまな ORM を含めることができます。ただし、ORM については別の議論の対象とします。すでに理解されているかもしれませんが、データ層が最初に登場しました。JDK 1.1 の時代から、JDBC (Java DataBase Connectivity - Java でのデータベースへの接続) が Java の世界に登場しました。これは、Java アプリケーションとさまざまな DBMS との対話のための標準であり、Java SE に含まれる java.sql および javax.sql パッケージの形式で実装されます。仕事の始まり
JDBC API が一般的に何であるかを理解するには、Java アプリケーションが必要です。プロジェクト アセンブリ システムのいずれかを使用するのが最も便利です。たとえば、Gradle を使用してみましょう。Gradle の詳細については、短いレビュー「Gradle の概要」を参照してください。まず、新しい Gradle プロジェクトを初期化しましょう。Gradle の機能はプラグインを通じて実装されるため、初期化には「 Gradle Build Init Plugin 」を使用する必要があります。gradle init --type java-application
この後、ビルド スクリプト - build.gradleファイルを開いてみましょう。このファイルには、プロジェクトとその操作方法が記述されています。私たちが興味があるのは、依存関係が記述されている「dependency」ブロックです。つまり、これらのライブラリなしでは作業できず、依存しているライブラリ/フレームワーク/API です。デフォルトでは、次のようなものが表示されます。
dependencies {
// This dependency is found on compile classpath of this component and consumers.
implementation 'com.google.guava:guava:26.0-jre'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
}
なぜここでこれが見られるのでしょうか? これらは、プロジェクトの作成時に Gradle が自動的に生成したプロジェクトの依存関係です。また、guava は Java SE には含まれていない別のライブラリであるためです。JUnit も Java SE には含まれていません。しかし、JDBC はそのまま使用できます。つまり、JDBC は Java SE の一部です。JDBC があることがわかりました。素晴らしい。他に何が必要ですか? こんな素晴らしい図があります。
dependencies {
implementation 'com.h2database:h2:1.4.197'
依存関係の検索方法とその記述方法は、データベース プロバイダーの公式 Web サイトまたは「 Maven Central 」 に記載されています。ご存知のとおり、JDBC ドライバーはデータベースではありません。しかし、彼は単なるガイドにすぎません。しかし、「インメモリデータベース」というものがあります。これらは、アプリケーションの存続期間中メモリ内に存在するデータベースです。通常、これはテストやトレーニングの目的でよく使用されます。これにより、マシンに別のデータベース サーバーをインストールする必要がなくなります。これは、JDBC について知るのに非常に適しています。これでサンドボックスの準備ができたので、始めましょう。
繋がり
つまり、JDBC ドライバーと JDBC API があります。覚えているとおり、JDBC は Java DataBase Connectivity の略です。したがって、すべては接続、つまり接続を確立する機能から始まります。そしてコネクションはコネクションです。もう一度JDBC 仕様の本文に戻り、目次を見てみましょう。「第 4 章 概要」 (概要)の章では、 「 4.1 接続の確立」 (接続の確立)セクションに移ります。データベースに接続するには 2 つの方法があると言われています。- DriverManager経由
- データソース経由
Connection con = DriverManager.getConnection(url, user, passwd);
パラメータは、選択したデータベースの Web サイトから取得できます。私たちの場合、これは H2 - 「H2 チートシート」です。Gradle が用意した AppTest クラスに移りましょう。JUnit テストが含まれています。JUnit テストは、アノテーションが付けられたメソッドです@Test
。単体テストはこのレビューのテーマではないため、これらは特定の方法で記述されたメソッドであり、その目的は何かをテストすることであるという理解に限定します。JDBC 仕様と H2 Web サイトに従って、データベースへの接続を受信したかどうかを確認します。接続を取得するメソッドを書いてみましょう。
private Connection getNewConnection() throws SQLException {
String url = "jdbc:h2:mem:test";
String user = "sa";
String passwd = "sa";
return DriverManager.getConnection(url, user, passwd);
}
次に、接続が実際に確立されていることを確認するこのメソッドのテストを作成しましょう。
@Test
public void shouldGetJdbcConnection() throws SQLException {
try(Connection connection = getNewConnection()) {
assertTrue(connection.isValid(1));
assertFalse(connection.isClosed());
}
}
このテストを実行すると、結果の接続が有効である (正しく作成された) こと、および接続が閉じられていないことが検証されます。try-with-resourcesを使用すると、リソースが不要になったときにリソースを解放できます。これにより、接続の低下やメモリ リークから保護されます。データベースとのアクションには接続が必要であるため、テストの開始時に @Test とマークされた残りのテスト メソッドに接続を提供し、テスト後にリリースします。これを行うには、@Before と @After という 2 つのアノテーションが必要です。テスト用の JDBC 接続を保存する新しいフィールドを AppTest クラスに追加しましょう。
private static Connection connection;
そして、新しいメソッドを追加してみましょう。
@Before
public void init() throws SQLException {
connection = getNewConnection();
}
@After
public void close() throws SQLException {
connection.close();
}
今後は、どのテスト メソッドでも JDBC 接続が保証され、毎回それ自体を作成する必要がなくなります。
ステートメント
次に、ステートメントまたは式に興味があります。これらについては、ドキュメントの「第 13 章 ステートメント」の章で説明されています。まず、ステートメントにはいくつかの種類またはタイプがあると述べています。- ステートメント: パラメーターを含まない SQL 式
- PreparedStatement : 入力パラメータを含む準備された SQL ステートメント
- CallableStatement: SQL ストアド プロシージャから戻り値を取得できる SQL 式。
private int executeUpdate(String query) throws SQLException {
Statement statement = connection.createStatement();
// Для Insert, Update, Delete
int result = statement.executeUpdate(query);
return result;
}
前の方法を使用してテスト テーブルを作成するメソッドを追加しましょう。
private void createCustomerTable() throws SQLException {
String customerTableQuery = "CREATE TABLE customers " +
"(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
String customerEntryQuery = "INSERT INTO customers " +
"VALUES (73, 'Brian', 33)";
executeUpdate(customerTableQuery);
executeUpdate(customerEntryQuery);
}
では、これをテストしてみましょう:
@Test
public void shouldCreateCustomerTable() throws SQLException {
createCustomerTable();
connection.createStatement().execute("SELECT * FROM customers");
}
次に、パラメーターを使用してリクエストを実行してみましょう。
@Test
public void shouldSelectData() throws SQLException {
createCustomerTable();
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
}
JDBC は PreparedStatement の名前付きパラメーターをサポートしていないため、パラメーター自体は質問によって指定され、値を指定することで質問のインデックス (0 ではなく 1 から始まる) を示します。最後のテストでは、結果があるかどうかの指標として true を受け取りました。しかし、クエリ結果は JDBC API でどのように表現されるのでしょうか? そして、それは ResultSet として表示されます。
結果セット
ResultSet の概念は、JDBC API 仕様の「第 15 章 結果セット」の章で説明されています。まず第一に、ResultSet は実行されたクエリの結果を取得および操作するためのメソッドを提供すると述べています。つまり、execute メソッドが true を返した場合、ResultSet を取得できます。createCustomerTable() メソッドの呼び出しを @Before としてマークされた init メソッドに移動しましょう。それでは、 shouldSelectData テストを完了しましょう。@Test
public void shouldSelectData() throws SQLException {
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
// Обработаем результат
ResultSet resultSet = statement.getResultSet();
resultSet.next();
int age = resultSet.getInt("age");
assertEquals(33, age);
}
ここで注目していただきたいのは、次はいわゆる「カーソル」を移動するメソッドです。ResultSet 内のカーソルは、ある行を指します。したがって、行を読むには、まさにその行にカーソルを置く必要があります。カーソルを移動すると、カーソルが有効 (正しい、正しい)、つまりカーソルがデータを指している場合、カーソル移動メソッドは true を返します。false を返す場合、データは存在しません。つまり、カーソルはデータを指していません。無効なカーソルを使用してデータを取得しようとすると、「データがありません」というエラーが表示されます。また、興味深いのは、ResultSet を使用して行を更新したり、行を挿入したりできることです。
@Test
public void shouldInsertInResultSet() throws SQLException {
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet = statement.executeQuery("SELECT * FROM customers");
resultSet.moveToInsertRow();
resultSet.updateLong("id", 3L);
resultSet.updateString("name", "John");
resultSet.updateInt("age", 18);
resultSet.insertRow();
resultSet.moveToCurrentRow();
}
行セット
ResultSet に加えて、JDBC では RowSet の概念が導入されています。詳細については、「JDBC の基本: RowSet オブジェクトの使用」を参照してください。使い方のバリエーションもいろいろあります。たとえば、最も単純なケースは次のようになります。@Test
public void shouldUseRowSet() throws SQLException {
JdbcRowSet jdbcRs = new JdbcRowSetImpl(connection);
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
jdbcRs.next();
String name = jdbcRs.getString("name");
assertEquals("Brian", name);
}
ご覧のとおり、RowSet はステートメント (コマンドを指定しました) と実行されたコマンドの共生に似ています。これを通じて (next メソッドを呼び出して) カーソルを制御し、カーソルからデータを取得します。このアプローチは興味深いだけでなく、実装の可能性も考えられます。たとえば、CachedRowSet。これは「切断」されており (つまり、データベースへの永続的な接続は使用されません)、データベースとの明示的な同期が必要です。
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
詳細については、Oracle Web サイトのチュートリアル「CachedRowSetObjects の使用」を参照してください。
メタデータ
クエリに加えて、データベースへの接続 (つまり、Connection クラスのインスタンス) により、データベースがどのように構成され編成されているかに関するデータであるメタデータへのアクセスが提供されます。その前に、いくつかの重要なポイントについて触れておきます。データベースに接続するための URL: 「jdbc:h2:mem:test」。test はデータベースの名前です。JDBC API の場合、これはディレクトリです。名前は大文字、つまり TEST になります。H2のデフォルトのスキーマはPUBLIC です。次に、すべてのユーザー テーブルを表示するテストを作成しましょう。なぜカスタムなのか?なぜなら、データベースにはユーザー テーブル (create table 式を使用して自分で作成したテーブル) だけでなく、システム テーブルも含まれるからです。これらは、データベースの構造に関するシステム情報を保存するために必要です。各データベースは、このようなシステム テーブルを異なる方法で保存できます。たとえば、H2 では、「INFORMATION_SCHEMA」スキーマに保存されます。興味深いことに、INFORMATION SCHEMA は一般的なアプローチですが、Oracle は別の道を歩みました。詳細については、「INFORMATION_SCHEMA と Oracle」を参照してください。ユーザーテーブルのメタデータを受け取るテストを書いてみましょう。@Test
public void shoudGetMetadata() throws SQLException {
// У нас URL = "jdbc:h2:mem:test", где test - название БД
// Название БД = catalog
DatabaseMetaData metaData = connection.getMetaData();
ResultSet result = metaData.getTables("TEST", "PUBLIC", "%", null);
List<String> tables = new ArrayList<>();
while(result.next()) {
tables.add(result.getString(2) + "." + result.getString(3));
}
assertTrue(tables.contains("PUBLIC.CUSTOMERS"));
}
接続プール
JDBC 仕様の接続プールには、「第 11 章 接続プーリング」というセクションがあります。これは、接続プールの必要性を正当化する主な理由にもなります。各接続はデータベースへの物理接続です。その作成と終了は非常に「高価な」仕事です。JDBC は接続プーリング API のみを提供します。したがって、実装の選択は依然として私たちにあります。たとえば、そのような実装には、HikariCP が含まれます。したがって、プロジェクトの依存関係にプールを追加する必要があります。dependencies {
implementation 'com.h2database:h2:1.4.197'
implementation 'com.zaxxer:HikariCP:3.3.1'
testImplementation 'junit:junit:4.12'
}
次に、このプールを何らかの方法で使用する必要があります。これを行うには、データ ソース (データソースとも呼ばれます) を初期化する必要があります。
private DataSource getDatasource() {
HikariConfig config = new HikariConfig();
config.setUsername("sa");
config.setPassword("sa");
config.setJdbcUrl("jdbc:h2:mem:test");
DataSource ds = new HikariDataSource(config);
return ds;
}
そして、プールから接続を受信するテストを書いてみましょう。
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
DataSource datasource = getDatasource();
try (Connection con = datasource.getConnection()) {
assertTrue(con.isValid(1));
}
}
トランザクション
JDBC で最も興味深い点の 1 つはトランザクションです。JDBC 仕様では、「第 10 章 トランザクション」の章が割り当てられています。まず第一に、トランザクションとは何かを理解することが重要です。トランザクションは、データに対する一連の操作を論理的に組み合わせたもので、全体として処理またはキャンセルされます。JDBCを使用する場合、トランザクションはいつ開始されますか? 仕様に記載されているように、これは JDBC ドライバーによって直接処理されます。ただし、通常、新しいトランザクションは、現在の SQL ステートメントがトランザクションを必要とし、トランザクションがまだ作成されていないときに開始されます。取引はいつ終了しますか? これは auto-commit 属性によって制御されます。自動コミットが有効になっている場合、トランザクションは SQL ステートメントが「完了」した後に完了します。「完了」が何を意味するかは、SQL 式のタイプによって異なります。- DML (挿入、更新、削除) とも呼ばれるデータ操作言語。
アクションが完了するとすぐにトランザクションが完了します。 Select ステートメント
- CallableStatement と複数の結果を返す式
関連するすべての ResultSet が閉じられ、すべての出力が受信されたとき (更新の数を含む)
トランザクションは、ResultSet が閉じられると完了します ( ResultSet#close )。
@Test
public void shouldCommitTransaction() throws SQLException {
connection.setAutoCommit(false);
String query = "INSERT INTO customers VALUES (1, 'Max', 20)";
connection.createStatement().executeUpdate(query);
connection.commit();
Statement statement = connection.createStatement();
statement.execute("SELECT * FROM customers");
ResultSet resultSet = statement.getResultSet();
int count = 0;
while(resultSet.next()) {
count++;
}
assertEquals(2, count);
}
それは簡単です。ただし、これはトランザクションが 1 つだけである限り当てはまります。それらが複数ある場合はどうすればよいですか? それらは互いに隔離する必要があります。したがって、トランザクション分離レベルと、JDBC がそれらをどのように処理するかについて説明しましょう。
断熱レベル
JDBC 仕様のサブセクション「10.2 トランザクション分離レベル」を開いてみましょう。ここで、先に進む前に、ACID などについて思い出しておきたいと思います。ACID はトランザクション システムの要件を記述します。- 原子性:
システムに部分的にコミットされるトランザクションはありません。そのサブ操作がすべて実行されるか、何も実行されないかのどちらかです。 - 一貫性:
成功した各トランザクションは、定義上、有効な結果のみを記録します。 - 分離:
トランザクションの実行中、同時トランザクションがその結果に影響を与えるべきではありません。 - 耐久性:
トランザクションが正常に完了した場合、トランザクションに加えられた変更は、失敗によって元に戻されることはありません。
ext {
h2Version = '1.3.176' // 1.4.177
hikariVersion = '3.3.1'
junitVersion = '4.12'
}
次に、これをバージョンで使用します。
dependencies {
implementation "com.h2database:h2:${h2Version}"
implementation "com.zaxxer:HikariCP:${hikariVersion}"
testImplementation "junit:junit:${junitVersion}"
}
h2 バージョンが低くなっていることに気づいたかもしれません。その理由については後で説明します。では、分離レベルをどのように適用するのでしょうか? すぐに小さな実用的な例を見てみましょう。
@Test
public void shouldGetReadUncommited() throws SQLException {
Connection first = getNewConnection();
assertTrue(first.getMetaData().supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
first.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
first.setAutoCommit(false);
// Транзакиця на подключение. Поэтому первая транзакция с ReadUncommited вносит изменения
String insertQuery = "INSERT INTO customers VALUES (5, 'Max', 15)";
first.createStatement().executeUpdate(insertQuery);
// Вторая транзакция пытается их увидеть
int rowCount = 0;
JdbcRowSet jdbcRs = new JdbcRowSetImpl(getNewConnection());
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
while (jdbcRs.next()) {
rowCount++;
}
assertEquals(2, rowCount);
}
興味深いことに、TRANSACTION_READ_UNCOMMITTED (sqlite や HSQL など) をサポートしていないベンダーでは、このテストが失敗する可能性があります。また、トランザクション レベルが機能しない可能性もあります。H2 データベース ドライバーのバージョンを示したことを覚えていますか? h2Version = '1.4.177' 以上にすると、コードは変更していませんが、READ UNCOMMITTED は機能しなくなります。これは、ベンダーとドライバーのバージョンの選択が単なる文字ではなく、実際にリクエストがどのように実行されるかを決定することを再度証明しています。バージョン 1.4.177 でこの動作を修正する方法と、それより上位のバージョンで動作しない方法については、「MVStore モードでの READ UNCOMMITTED 分離レベルのサポート」を参照してください。
結論
ご覧のとおり、JDBC は Java がデータベースを操作するための強力なツールです。この短いレビューが出発点を与えたり、記憶をリフレッシュするのに役立つことを願っています。さて、軽食として、いくつかの追加材料:- 火災レポート: Martin Kleppmann 氏の「取引: 神話、驚き、機会」
- ユーリ・トカッチ: 「JPA.トランザクション」
- Yurik Tkach: 「JDBC - テスターのための Java」
- Udemyの無料コース:「JDBCとMySQL」
- 「CallableStatement オブジェクトの処理」
- IBM 開発者: 「Java データベース接続」
- IBM Knowledge Center: 「JDBC 入門」
GO TO FULL VERSION