Apache Commons DbUtilsというライブラリを使うと、JDBCのAPIを直接呼び出すよりも手軽に、DBアクセスのコードが書けるようになります。
Commons DbUtilsは、大がかりなフレームワークではなくちょっとしたライブラリですので、導入しやすいことが利点です。
Jarを入手する
公式サイトにあるダウンロードのページからJarをダウンロードすることもできますが、Mavenを使っているならPOMに以下のように書けばOKです(つい最近、3年振りの最新バージョンである1.7がリリースされたようです)。
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
呼び出し方
Commons DbUtilsの基本的な呼び出し方を紹介します。
はじめにQueryRunnerを作る
はじめに、QueryRunnerというクラスのインスタンスを作ります。
単純に引数なしでnewするか、(コネクションプーリングなどのために)DataSourceを使っている場合は、それを引数にしてnewするだけです。
QueryRunner queryRunner = new QueryRunner();
// 何らかの手段でDataSourceがあるならこちら。
DataSource dataSource = ...
QueryRunner queryRunner = new QueryRunner(dataSource);
INSERT, UPDATE, DELETE文を実行する
次に、SQL文を実行するメソッドの中から、まずは単純でわかりやすい例を紹介します。
直接のJDBC呼び出しではPreparedStatement#executeUpdate()を使っていた部分の置き換えです。
具体的には、これだけです。
// Personというテーブルがあるとして、そのうちの1行をUPDATEする例
int rows = queryRunner.update(connection, "UPDATE Person SET Name = ? WHERE Id = ?", "Taro", 1);
こうするとQueryRunnerが内部で、PreparedStatementを作って、パラメータをセットして、executeUpdate()を呼んで、クローズする一連の処理を実行してくれます。
返り値は、PreparedStatement#executeUpdate()の返り値がそのまま、つまり変更された行数が返ります。
先頭の引数connectionは、QueryRunnerを作る時にDataSourceを渡していれば、省略可能です。
省略すると、内部でそのDataSourceに対してgetConnection()が呼ばれて使われます。そして自動的にクローズされます。
SQL文のパラメータは可変長引数として受け取ってくれますので、例えば以下のようにもできます。
List<Object> params = new ArrayList<>();
params.add(...);
params.add(...);
queryRunner.update("...", params.toArray()); // 配列の形にして渡せばOK。
SELECT文を実行する
続いて、SELECT文の実行について紹介します。
直接のJDBC呼び出しではPreparedStatement#executeQuery()を使っていた部分の置き換えです。
こちらは先ほどと違い、SQL文を実行するとResultSetが返りますので、そのResultSetからどのようにして所望の形(型)で結果をとりだすか、という変換が必要となります。そのために、Common DbUtilsにはResultSetHandlerというインターフェースを用意しています。
簡単な例として、特定のテーブルの、特定のレコードの、特定のカラムの文字列値(つまり単一のString)を取り出したい場合、以下のようになります。
// ResultSetHandlerを実装する。この例では匿名クラスとして実装した。
// ResultSetHandlerには型パラメータがあり、ここに返り値としたい型を指定する。今回はString。
ResultSetHandler<String> personNameHandler = new ResultSetHandler<String>() {
@Override
public String handle(ResultSet rs) throws SQLException {
if (rs.next()) {
// 1番目の列(しかないが)の値をStringとして返すだけ。
return rs.getString(1);
}
return null;
}
};
// ResultSetHandlerの実装クラスをResultSetHandler<String>として実装したので、
// 返り値nameの型はStringとなる。
String name = queryRunner.query(connection, "SELECT Name FROM Person WHERE Id = ?", personNameHandler, 1);
このように、「ResultSetが得られたら、それをどのように変換するか」という変換処理をResultSetHandlerとして表現し、それを引数に含めてQueryRunner#query()というメソッドを呼び出します。
こうするとQueryRunnerが内部で、PreparedStatement#executeQuery()を呼び、返ってきたResultSetにResultSetHandler#handle()を適用し、その結果を返すという一連の処理を実行してくれます。
ResultSetHandler以外の引数については、QueryRunner#update()と同様です(connectionを省略したり、パラメータを可変長引数で渡したり)。
あらかじめ用意されているResultSetHandlerの実装クラス
今回はResultSetHandlerを自分で実装しましたが、この例のように典型的な場面で使うResultSetHandler実装クラスは、既にCommons DbUtilsの中に用意されています。
詳細は公式のJavadocを読む必要がありますが、いくつか紹介します。
ScalarHandlerは、今回の例のように単一の値を取り出したい場面で使えます。
// 先ほどの例は、実はこれだけでOKだった。
String name = queryRunner.query("...", new ScalarHandler<>(), ...);
MapHandlerは、1行をSELECTした結果をマップに変換します。
カラム名がマップのキーになります。
Map<String, Object> map = queryRunner.query("SELECT * FROM Person WHERE Id = ?", new MapHandler(), 1);
// このようなマップが得られる。
// map.get("Id") -> 1
// map.get("Name") -> "Taro"
BeanHandlerは、1行をSELECTした結果をJavaBeanに変換します。
// このようなクラスが定義されているとして・・・
public class Person {
private int id;
private String name;
// 以下、getter/setterの定義・・・
}
// こうすると、SELECTした結果に応じてJavaのフィールドがセットされる。
Person person = queryRunner.query("SELECT * FROM Person WHERE Id = ?", new BeanHandler<>(Person.class), 1);
BeanHandlerのデフォルトでは、DBのカラム名とJavaのプロパティ名(フィールド名)が一致している必要があります。
以下のようにすることで、カラム名とプロパティ名の対応をマップとして与えることもできます。
// このようなクラスが定義されているとして・・・
public class Person2 {
private int id;
private String fullName; // ここがDBのカラム名と一致しない。
// 以下、getter/setterの定義・・・
// DBのカラム名をキー、Javaのプロパティ名を値とするマップを用意する。
Map<String, String> columnToPeroperty = new HashMap<>();
columnToPeroperty.put("Id", "id");
columnToPeroperty.put("Name", "fullName");
// BeanHandlerのコンストラクタ引数には、RowProcessorという型のオブジェクトを指定できる。
// さらに、RowProcessorの実装クラスであるBasicRowProcessorのコンストラクタ引数には、
// BeanProcessorという型のオブジェクトを指定できる。
// さらにさらに、BeanProcessorのコンストラクタ引数として、先ほどのマップを渡せる。
// 以上を、ボトムアップで作っていくと・・・
BeanProcessor beanProcessor = new BeanProcessor(columnToPeroperty);
RowProcessor rowProcessor = new BasicRowProcessor(beanProcessor);
ResultSetHandler<Person2> beanHandler = new BeanHandler<>(Person2.class, rowProcessor);
よく使いそうなResultSetHandler実装クラスは以上です。
他にも、複数行をSELECTした結果を解釈するために、1行の結果を要素として、それのリストやマップを返すクラスが用意されています。
INSERT文を実行したときのauto generated keyを取得する
テーブルによっては、INSERTの際に自動的に連番が振られたりするカラムもしばしばあるでしょう。
INSERTをすると同時にそうしたカラムの値を取得したい場面では、QueryRunner#update()の代わりにinsert()が役立ちます。
使い方はSELECTするときに使ったquery()と似ており、引数にResultSetHandlerを指定します。
そうするとQueryRunnerが内部で、PreparedStatement#executeUpdate()を呼んで、次にPreparedStatement#getGeneratedKeys()を呼んでResultSetを取得し、それに対してResultSetHandlerを適用し、その結果を返すという一連の処理を実行してくれます。
簡単な例を以下に示します。
// PersonテーブルにはIdというカラムがあり、INSERTすると連番が振られるという想定。
// ここではScalarHandlerを使った。
Integer taroId = queryRunner.insert("INSERT INTO Person(name) VALUES(?)", new ScalarHandler<>(), "Taro") ;