JDBCを使ってみた

JavaのデータベースアクセスAPIであるJDBCを使ってみました。

この文章はatmarkitのhttps://atmarkit.itmedia.co.jp/ait/articles/0106/26/news001.htmlを参考にしています。

JDBCはRDBMSのフロントエンドとして、SQL文をデータベースに送って、結果を受け取るAPIと考えてよさそうです。

JDBCを使用するにはJDBCドライバを用意する必要があります。MySQLのConnector/J 8.0.28を使用する場合、Mavenのdependeciesに以下の行を追加します。

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>8.0.28</version>

</dependency>

これはhttps://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.28からコピーしてきました。

JDBCドライバをロードするとデータベースアクセスが可能になるのですが、それには以下のコードを使用します。

Class.forName(ドライバクラス名);

ただし、MySQL Connector/Jは以下のコードを使用することを推奨しています。

Class.forName("com.mysql.cj.jdbc.Driver").newInstance();

これはhttps://dev.mysql.com/doc/connector-j/8.0/en/connector-j-usagenotes-connect-drivermanager.htmlに記載があります。

この操作によってJDBCのDriverManagerがJDBCドライバを検出することが可能になります。

次にデータベースとの接続を行います。

conn = DriverManager.getConnection(データベースURL, ユーザ名, パスワード);

java.sql.DriverManagerのgetConnectionはJDBCドライバを検索して、指定されたデータベースに指定のデータベースユーザで接続を行います。

戻り値はjava.sql.Connection型となります。なお、後でも触れますが、戻り値connに返された接続は例外発生時にも必ずクローズ処理をしなければならないので、データベースアクセスはtry節で囲み、connの宣言はtry節の外に配置して、catch節で例外を捕捉した際にクローズできるようにするします。

データベースへのアクセスにはステートメントを使用します。

stmt = conn.createStatement();

戻り値はjava.sql.Statement型で、接続と同様にステートメントも確実にクローズする必要があります。

単にクエリを発行するだけであれば、ステートメントはステートメントは無駄に1階層多いだけですが、 SQL文のプリコンパイルによる高速化のために用意されたpreparedStatementと整合を図るためにステートメント型を用意したのかと考えています。

データベースの検索には以下のコードを使用します。

rset = stmt.executeQuery(SQL文);

引数にはSQL文をそのまま渡します。残念ながらSQLを勉強しなくてもデータベースが扱えるというわけにはいきません。

結果はjava.sql.ResultSet型で返されます。

データベースの検索結果は1通りだけとは限りませんので、結果の取得は以下のコードで実行します。

while (rset.next()) {
  // 各行の処理
  item = rset.getString(1); // 整数値の読み取り

  price = rset.getInt("PRICE"); // 文字列の読み取り

  ...

}

ResultSetがexecuteQueryから返された直後、カーソルは最初の行の直前を指しています。next()を呼ぶことで最初の行に移動し、その行があればtrueを返すので、行内のデータを読み取ることができます。

行内のデータ読みとりにはgetString(1)のようにデータベースのカラムの型に対応した読み取り関数を使用し、カラム位置を指定して読み出しを行います。

また、カラム位置の代わりにgetInt("PRICE")のようにデータベースのカラム名を文字列で指定することもできます。

insert (行の追加)、update (行の更新)など結果を返さないSQL文を実行する場合にはexecuteQueryではなくexecuteUpdateを使用します。

データベース処理が終了したらステートメントと接続をクローズする必要があります。基本的には以下のようなパターンで接続をクローズします。

Conncetion conn = null;
Statement stmt = null;

try {

  Class.forName(....);

  conn = DriverManager.getConnection(...);

  stmt = conn.createStatement(...);

  ...

  stmt.close();

  conn.close();

} catch {

  ...

  try (SQLException se) {

    if (stmt != null) {

      stmt.close();

    }

    if (conn != null) {

      conn.close();

    }

  } catch (SQLException se) {}

}

参考にしたWebサイトでは上記の書き方になっていましたが、close()をfinally節に記述するのもアリな気がします。

プリペアド・ステートメントによるクエリの高速化

SQL文は文字列なのでインタープリタやコンパイラのように字句解析や文法解析が必要になります。また、JOINを使用した複雑なクエリでは検索処理の最適化も有用です。

繰り返しの検索を行う場合、こういった処理はパフォーマンス低下を引き起こす可能性があります。

これを避けるための手段としてプリペアド・ステートメントが用意されています。

プリペアド・ステートメントを要約するとSQL文をプリコンパイルしておいて、クエリの際にはコンパイル済のSQL文を使用するということのようです。

プリペアド・ステートメントを使用するには以下のコードを使用します。

pstmt = conn.prepareStatement(SQL文);

戻り値の型はjava.sql.PreparedStatement型で、クエリを実行する際には以下のように引数なしでexecuteQueryを実行します。

rset = pstmt.executeQuery();

ところで、繰り返しの検索と言っても毎回同じ検索を行うわけではなく、検索対象が変化する場合が普通です。

これに対応するため、プリペアド・ステートメントでは?による置き換えが可能になっています。以下に例を示します。

pstmt = conn.prepareStatement("select PRICE from PRICELIST where ITEM = ?");
pstmt.setString(1, "PENCIL");

setStringは指定の?パラメータを指定の文字列で置き換える指示で、データベースカラムの型に合わせてsetIntなどいくつかの関数が用意されています。引数の1番目は置換対象の?パラメータの位置で1始まり、2番目は置換する値です。

プリペアド・ステートメントはプリコンパイルされていることが前提でSQL文の文字列を置き換えるわけではないので、置換指示関数はデータベースカラムの型に従っている必要があります。

プリペアド・ステートメントもステートメント同様に確実にクローズする必要がある点に留意が必要です。

ステートメントとプリペアド・ステートメントを同時に使えるかはまだ確認していませんが、使えないと検索してアップデートという処理でとても困るので、同時に使えるだろうと考えて簡単なプログラムでは問題ないことを確認しましたが、ちょっと調べたところそういう例がないところが困り物です。

最後に

今回は考察していませんが、データベースアクセスではセキュリティというのが重要になります。SQL文に埋め込む値に不正な文字 (例えば";") が入っているとデータベースを不正に操作できるようになってしまいます。

これを避けるには入力文字列のクレンジングや適切なデータベース権限の設定、サーバー自体のセキュリティ確保が必要です。

この点については後々考えていきたいと思います。



0コメント

  • 1000 / 1000