Apache Commons DbUtils

Apache CommonsのDbUtilsJDBCの補助ライブラリで、よく書くテンプレを簡略化することができます。DbUtilsを使うことでConnectionのcloseし忘れを防止することができたりもします。自分はQMACloneのデータベース部分にDbUtilsを使っています。

準備

解説では以下のMySQLのテーブルとJavaのclassを使います。

CREATE TABLE person (
  id SERIAL PRIMARY KEY,
  name TEXT
);
public class Person {
  public long id;
  public String name;

  public long getId() { return id; }
  public void setId(long id) { this.id = id; }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
};

初期化

DbUtilsJDBCを扱うときはQueryRunnerを使用します。自分はApache CommonsのDBCPで作成した接続プールと一緒に使っています。

QueryRunner runner = new QueryRunner(databaseConnectionPool.getDataSoure());

INSERT文・UPDATE文・REPLACE文・DELETE文

INSERT文・UPDATE文・REPLACE文・DELETE文はQueryRunner#update()を使って実行します。update()は第一引数にPreparedStatementに渡すものと同じ書式のSQL文、第二匹数以降にSQL文の"?"に当てはめる値を順に指定していきます。

runner.update("INSERT person SET name = ?", "hoge");
runner.update("UPDATE person SET name = ? WHERE id = ?", "fuga", 1);
runner.update("DELETE person WHERE id = ?", 1);

update()の内部では

  1. Connectionの確立
  2. PreparedStatementへのパラメーター指定
  3. SQL文の実行
  4. Connectionのclose

が行われています。自分でこれらの動作を書く必要がない分、コードが簡潔になっています。また、接続のcloseし忘れの心配もありません。

バッチ処理

INSERT文・UPDATE文・REPLACE文・DELETE文はQueryRunner#batch()を使ってバッチ処理することができます。batch()には第一引数にupdate()と同様にSQL文、第二引数にはSQL文の"?"に当てはめる値を順に格納したObjectの配列、Object[]を渡します。

List<Object[]> params = new ArrayList<Object[]>();
params.add(new Object[]{"hoge"});
params.add(new Object[]{"fuga"});
runner.batch("INSERT person SET name = ?", params.toArray(new Object[0][]));

batch()の内部ではupdate()と同様に

  1. Connectionの確立
  2. PreparedStatementへのパラメーター指定
  3. バッチ追加
  4. バッチ実行
  5. Connectionのclose

が行われています。

SELECT文

SELECT文はQueryRunner#query()を使って実行します。ResultSetHandlerインスタンスを渡して、handle()メソッドに渡されてきたResultSetから値を取り出します。handle()はT型を返すことができるようになっており、ここで返した値がquery()の返り値になります。

Person person = runner.query("SELECT * FROM person WHERE id = ?", new ResultSetHandler<Person>() {
  @Override
  public Person handle(ResultSet rs) throws SQLException {
    if (rs.next()) {
      Person person = new Person();
      person.id = rs.getLong("id");
      person.name = rs.getString("name");
      return person;
    }
    return null;
  }
}, 1);
List<Person> people = runner.query("SELECT * FROM person", new ResultSetHandler<List<Person>>() {
  @Override
  public List<Person> handle(ResultSet rs) throws SQLException {
    List<Person> people = new ArrayList<>();
    while (rs.next()) {
      Person person = new Person();
      person.id = rs.getLong("id");
      person.name = rs.getString("name");
      people.add(person);
    }
    return people;
  }
});

query()の内部では

  1. Connectionの確立
  2. PreparedStatementへのパラメーター指定
  3. SQL文の実行
  4. ResultSetのclose
  5. Connectionのclose

が行われています。ResultSetHandlerを使用する場合、handle()には何も操作がされてない状態のResultSetが渡されます。

AbstractListHandler

query()の戻り値をListにしたい場合、AbstractListHandlerで一部のコードを省略できる場合があります。

List<Person> people = runner.query("SELECT * FROM person", new AbstractListHandler<Person>() {
  @Override
  protected Person handleRow(ResultSet rs) throws SQLException {
    Person person = new Person();
    person.id = rs.getLong("id");
    person.name = rs.getString("name");
    return person;
  }
});

AbstractListHandlerArrayListインスタンスを生成し、handleRow()の戻り値をadd()し、query()の返り値とします。先ほどよりもコードが簡潔になっています。個人的にはAbstractCollectionHandlerという形で、自分で戻り値となる型を指定できるようにして欲しかったのですが、戻り値の方のキャストが必要になる場合が多いので一長一短かもしれません。createArray()でListのインスタンスを選べるようにするくらいはして欲しかったです。

AbstractKeyedHandler

query()の戻り値をMapにしたい場合、AbstractKeyedHandlerで一部のコードを省略できる場合があります。

Map<Long, Person> people = runner.query("SELECT * FROM person",
    new AbstractKeyedHandler<Long, Person>() {
      @Override
      protected Person createRow(ResultSet rs) throws SQLException {
        Person person = new Person();
        person.id = rs.getLong("id");
        person.name = rs.getString("name");
        return person;
      }

      @Override
      protected Long createKey(ResultSet rs) throws SQLException {
        return rs.getLong("id");
      }
    });

AbstractListHandlerはprotectedメソッドのcreateMap()でMapのインスタンスを生成し、ResultSetの各行についてcreateKey()とcreateRow()でキーと値を取り出し、Mapにput()していきます。こちらはcreateMap()をオーバーライドすることでMapのインスタンスを選べるようになっています。

ColumnListHandler

ある列のListが欲しい場合はColumnListHandlerを使うこともできます。ただしquery()の戻り値がListとなってしまうため、使いどころが難しいです。

List<Object> people = runner.query("SELECT * FROM person", new ColumnListHandler(name));

ColumnListHandlerはコンストラクタで指定した列の値のListを返してくれます。引数を省略した場合は1列目の結果のListを返してくれるので、以下のような簡潔な書き方もできます。

List<Object> people = runner.query("SELECT name FROM person", new ColumnListHandler());

SQLのint(10)型がJavaのLong型で返されてしまうため、自分は以下のようにカスタマイズしたColumnListHandlerを使っています。

public class ColumnListHandler<T> extends AbstractListHandler<T> {
  private final Class<T> c;
  public ColumnListHandler(Class<T> c) { this.c = c; }
  protected T handleRow(ResultSet rs) throws SQLException {
    Object object = rs.getObject(1);
    if (c == object.getClass()) {
      return c.cast(object);
    } else if (c == Integer.class && object.getClass() == Long.class) {
      // SQL の int(10) 型を Java の Long 型で返すため、特別処理
      return c.cast((Integer) (int) (long) (Long) object);
    } else {
      throw new IllegalArgumentException(String.format("%s から %s にキャストできません", object
          .getClass().toString(), c.toString()));
    }
  }
}

ScalarHandler

ResultSetの初めの一行の特定の列の値が欲しい場合はScalarHandlerを使うことができます。

long count = (long) runner.query("SELECT COUNT(*) FROM person", new ScalarHandler());

ScalarHandlerはResultSetの初めの行の特定の列の値を返してくれます。自分は上のようにCOUNT()やMAX()の値を取り出す時に使っています。

BeanHandler・BeanListHandler

Beansに対応したアクセサメソッドが定義されているclassであれば、自分でResultSetHandlerを書かずに値を取り出すことができます。

Person person = runner.query("SELECT * FROM person WHERE ID = ?", new BeanHandler<>(
    Person.class), 1);
List<Person> porple = runner.query("SELECT * FROM person", new BeanListHandler<>(
    Person.class));

BeanHandler・BeanListHandlerはBeanProcessorを使ってResultSetを指定したクラスのオブジェクトに変換して取り出してくれます。楽です。ただし、アクセサを書かなければならないのだ若干億劫です。

まとめ

JDBCを使うとき、Apache CommonsのDbUtilsを使うと定型文を省略して書くことができ、コーディングの効率を上げることができます。

リンク