Apache Commons DbUtilsガイド

Apache Commons DbUtilsのガイド

1. 概要

Apache Commons DbUtilsは、JDBCを簡単に操作できる小さなライブラリです。

この記事では、その特徴と機能を紹介する例を実装します。

2. セットアップ

2.1. Mavenの依存関係

まず、commons-dbutilsh2の依存関係をpom.xmlに追加する必要があります。


    commons-dbutils
    commons-dbutils
    1.6


    com.h2database
    h2
    1.4.196

commons-dbutilsh2の最新バージョンはMavenCentralで見つけることができます。

2.2. テストデータベース

依存関係が整ったら、使用するテーブルとレコードを作成するスクリプトを作成しましょう。

CREATE TABLE employee(
    id int NOT NULL PRIMARY KEY auto_increment,
    firstname varchar(255),
    lastname varchar(255),
    salary double,
    hireddate date,
);

CREATE TABLE email(
    id int NOT NULL PRIMARY KEY auto_increment,
    employeeid int,
    address varchar(255)
);

INSERT INTO employee (firstname,lastname,salary,hireddate)
  VALUES ('John', 'Doe', 10000.10, to_date('01-01-2001','dd-mm-yyyy'));
// ...
INSERT INTO email (employeeid,address)
  VALUES (1, '[email protected]');
// ...

この記事のすべてのサンプルテストケースでは、H2インメモリデータベースへの新しく作成された接続を使用します。

public class DbUtilsUnitTest {
    private Connection connection;

    @Before
    public void setupDB() throws Exception {
        Class.forName("org.h2.Driver");
        String db
          = "jdbc:h2:mem:;INIT=runscript from 'classpath:/employees.sql'";
        connection = DriverManager.getConnection(db);
    }

    @After
    public void closeBD() {
        DbUtils.closeQuietly(connection);
    }
    // ...
}

2.3. POJO

最後に、2つの簡単なクラスが必要です。

public class Employee {
    private Integer id;
    private String firstName;
    private String lastName;
    private Double salary;
    private Date hiredDate;

    // standard constructors, getters, and setters
}

public class Email {
    private Integer id;
    private Integer employeeId;
    private String address;

    // standard constructors, getters, and setters
}

3. 前書き

DbUtilsライブラリは、利用可能なほとんどの機能に対してthe QueryRunner class as the main entry pointを提供します。

このクラスは、データベースへの接続、実行するSQLステートメント、およびクエリのプレースホルダーに値を提供するオプションのパラメーターリストを受け取ることで機能します。

後で説明するように、いくつかのメソッドはResultSetHandler実装も受け取ります。これは、ResultSetインスタンスをアプリケーションが期待するオブジェクトに変換する役割を果たします。

もちろん、ライブラリには、リスト、マップ、JavaBeansなど、最も一般的な変換を処理するいくつかの実装が既に用意されています。

4. データのクエリ

基本がわかったので、データベースにクエリを実行する準備が整いました。

MapListHandlerを使用して、データベース内のすべてのレコードをマップのリストとして取得する簡単な例から始めましょう。

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedList()
  throws SQLException {
    MapListHandler beanListHandler = new MapListHandler();

    QueryRunner runner = new QueryRunner();
    List> list
      = runner.query(connection, "SELECT * FROM employee", beanListHandler);

    assertEquals(list.size(), 5);
    assertEquals(list.get(0).get("firstname"), "John");
    assertEquals(list.get(4).get("firstname"), "Christian");
}

次に、BeanListHandlerを使用して結果をEmployeeインスタンスに変換する例を次に示します。

@Test
public void givenResultHandler_whenExecutingQuery_thenEmployeeList()
  throws SQLException {
    BeanListHandler beanListHandler
      = new BeanListHandler<>(Employee.class);

    QueryRunner runner = new QueryRunner();
    List employeeList
      = runner.query(connection, "SELECT * FROM employee", beanListHandler);

    assertEquals(employeeList.size(), 5);
    assertEquals(employeeList.get(0).getFirstName(), "John");
    assertEquals(employeeList.get(4).getFirstName(), "Christian");
}

単一の値を返すクエリの場合、ScalarHandlerを使用できます。

@Test
public void givenResultHandler_whenExecutingQuery_thenExpectedScalar()
  throws SQLException {
    ScalarHandler scalarHandler = new ScalarHandler<>();

    QueryRunner runner = new QueryRunner();
    String query = "SELECT COUNT(*) FROM employee";
    long count
      = runner.query(connection, query, scalarHandler);

    assertEquals(count, 5);
}

すべてのResultSerHandlerの実装については、ResultSetHandler documentationを参照してください。

4.1. カスタムハンドラー

結果をオブジェクトに変換する方法をさらに制御する必要がある場合は、QueryRunnerのメソッドに渡すカスタムハンドラーを作成することもできます。

これは、ResultSetHandlerインターフェイスを実装するか、ライブラリによって提供される既存の実装の1つを拡張することによって実行できます。

2番目のアプローチがどのように見えるか見てみましょう。 まず、Employeeクラスに別のフィールドを追加しましょう。

public class Employee {
    private List emails;
    // ...
}

それでは、BeanListHandlerタイプを拡張し、各従業員のメーリングリストを設定するクラスを作成しましょう。

public class EmployeeHandler extends BeanListHandler {

    private Connection connection;

    public EmployeeHandler(Connection con) {
        super(Employee.class);
        this.connection = con;
    }

    @Override
    public List handle(ResultSet rs) throws SQLException {
        List employees = super.handle(rs);

        QueryRunner runner = new QueryRunner();
        BeanListHandler handler = new BeanListHandler<>(Email.class);
        String query = "SELECT * FROM email WHERE employeeid = ?";

        for (Employee employee : employees) {
            List emails
              = runner.query(connection, query, handler, employee.getId());
            employee.setEmails(emails);
        }
        return employees;
    }
}

コンストラクターにConnectionオブジェクトが必要であることに注意してください。これにより、クエリを実行して電子メールを取得できます。

最後に、コードをテストして、すべてが期待どおりに機能しているかどうかを確認しましょう。

@Test
public void
  givenResultHandler_whenExecutingQuery_thenEmailsSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);

    QueryRunner runner = new QueryRunner();
    List employees
      = runner.query(connection, "SELECT * FROM employee", employeeHandler);

    assertEquals(employees.get(0).getEmails().size(), 2);
    assertEquals(employees.get(2).getEmails().size(), 3);
}

4.2. カスタム行プロセッサ

この例では、employeeテーブルの列名は、Employeeクラスのフィールド名と一致します(一致は大文字と小文字を区別しません)。 ただし、常にそうであるとは限りません。たとえば、列名で複合語を区切るためにアンダースコアが使用されている場合などです。

このような状況では、RowProcessorインターフェースとその実装を利用して、列名をクラスの適切なフィールドにマップできます。

これがどのように見えるか見てみましょう。 まず、別のテーブルを作成して、そのテーブルにいくつかのレコードを挿入しましょう。

CREATE TABLE employee_legacy (
    id int NOT NULL PRIMARY KEY auto_increment,
    first_name varchar(255),
    last_name varchar(255),
    salary double,
    hired_date date,
);

INSERT INTO employee_legacy (first_name,last_name,salary,hired_date)
  VALUES ('John', 'Doe', 10000.10, to_date('01-01-2001','dd-mm-yyyy'));
// ...

それでは、EmployeeHandlerクラスを変更しましょう。

public class EmployeeHandler extends BeanListHandler {
    // ...
    public EmployeeHandler(Connection con) {
        super(Employee.class,
          new BasicRowProcessor(new BeanProcessor(getColumnsToFieldsMap())));
        // ...
    }
    public static Map getColumnsToFieldsMap() {
        Map columnsToFieldsMap = new HashMap<>();
        columnsToFieldsMap.put("FIRST_NAME", "firstName");
        columnsToFieldsMap.put("LAST_NAME", "lastName");
        columnsToFieldsMap.put("HIRED_DATE", "hiredDate");
        return columnsToFieldsMap;
    }
    // ...
}

BeanProcessorを使用して、列からフィールドへの実際のマッピングを実行していることに注意してください。アドレス指定が必要な場合のみです。

最後に、すべてが問題ないことをテストしましょう。

@Test
public void
  givenResultHandler_whenExecutingQuery_thenAllPropertiesSetted()
    throws SQLException {
    EmployeeHandler employeeHandler = new EmployeeHandler(connection);

    QueryRunner runner = new QueryRunner();
    String query = "SELECT * FROM employee_legacy";
    List employees
      = runner.query(connection, query, employeeHandler);

    assertEquals((int) employees.get(0).getId(), 1);
    assertEquals(employees.get(0).getFirstName(), "John");
}

5. レコードの挿入

QueryRunnerクラスは、データベースにレコードを作成するための2つのアプローチを提供します。

1つ目は、update()メソッドを使用して、SQLステートメントとオプションの置換パラメーターのリストを渡すことです。 このメソッドは、挿入されたレコードの数を返します。

@Test
public void whenInserting_thenInserted() throws SQLException {
    QueryRunner runner = new QueryRunner();
    String insertSQL
      = "INSERT INTO employee (firstname,lastname,salary, hireddate) "
        + "VALUES (?, ?, ?, ?)";

    int numRowsInserted
      = runner.update(
        connection, insertSQL, "Leia", "Kane", 60000.60, new Date());

    assertEquals(numRowsInserted, 1);
}

2つ目は、insert()メソッドを使用することです。このメソッドは、SQLステートメントと置換パラメーターに加えて、結果の自動生成されたキーを変換するためにResultSetHandlerを必要とします。 戻り値は、ハンドラーが返すものになります。

@Test
public void
  givenHandler_whenInserting_thenExpectedId() throws SQLException {
    ScalarHandler scalarHandler = new ScalarHandler<>();

    QueryRunner runner = new QueryRunner();
    String insertSQL
      = "INSERT INTO employee (firstname,lastname,salary, hireddate) "
        + "VALUES (?, ?, ?, ?)";

    int newId
      = runner.insert(
        connection, insertSQL, scalarHandler,
        "Jenny", "Medici", 60000.60, new Date());

    assertEquals(newId, 6);
}

6. 更新と削除

QueryRunnerクラスのupdate()メソッドを使用して、データベースのレコードを変更および消去することもできます。

その使用法は簡単です。 従業員の給与を更新する方法の例を次に示します。

@Test
public void givenSalary_whenUpdating_thenUpdated()
 throws SQLException {
    double salary = 35000;

    QueryRunner runner = new QueryRunner();
    String updateSQL
      = "UPDATE employee SET salary = salary * 1.1 WHERE salary <= ?";
    int numRowsUpdated = runner.update(connection, updateSQL, salary);

    assertEquals(numRowsUpdated, 3);
}

そして、指定されたIDを持つ従業員を削除するもう1つの方法は次のとおりです。

@Test
public void whenDeletingRecord_thenDeleted() throws SQLException {
    QueryRunner runner = new QueryRunner();
    String deleteSQL = "DELETE FROM employee WHERE id = ?";
    int numRowsDeleted = runner.update(connection, deleteSQL, 3);

    assertEquals(numRowsDeleted, 1);
}

7. 非同期操作

DbUtilsは、操作を非同期で実行するためのAsyncQueryRunnerクラスを提供します。 このクラスのメソッドは、Futureインスタンスを返すことを除いて、QueryRunnerクラスのメソッドと対応しています。

データベース内のすべての従業員を取得し、結果を取得するまで最大10秒待機する例を次に示します。

@Test
public void
  givenAsyncRunner_whenExecutingQuery_thenExpectedList() throws Exception {
    AsyncQueryRunner runner
      = new AsyncQueryRunner(Executors.newCachedThreadPool());

    EmployeeHandler employeeHandler = new EmployeeHandler(connection);
    String query = "SELECT * FROM employee";
    Future> future
      = runner.query(connection, query, employeeHandler);
    List employeeList = future.get(10, TimeUnit.SECONDS);

    assertEquals(employeeList.size(), 5);
}

8. 結論

このチュートリアルでは、Apache Commons DbUtilsライブラリの最も注目すべき機能について説明しました。

データをクエリし、さまざまなオブジェクトタイプに変換し、生成されたプライマリキーを取得するレコードを挿入し、指定された基準に基づいてデータを更新および削除しました。 また、AsyncQueryRunnerクラスを利用して、クエリ操作を非同期で実行しました。

そして、いつものように、この記事の完全なソースコードはover on Githubにあります。