tamuraです。

Javaでデータベースを使う場合、以下の2パターンが考えられます。

  1. ORMを使ってアクセスする
  2. SELECT等のクエリを直接投げる

DBUtils3はクエリを直接投げる際に多少楽になるように作ったライブラリです。

https://github.com/tamurashingo/dbutils3/blob/master/README-ja.md


使い方

ライブラリのロード

Mavenを使っている場合は以下の5行を追加します。

<dependency>  
    <groupId>com.github.tamurashingo.dbutil3</groupId>
    <artifactId>dbutil3</artifactId>
    <version>0.1.0</version>
</dependency>  

DBConnectionUtilインスタンスの生成

接続済みのConnectionを元に、DBConnectionUtil(ライブラリで使用するコネクション)を生成します。

try (DBConnectionUtil conn = new DBConnectionUtil(connection)) {  
}

SQLを発行する

SQLを発行するにはプリコンパイル、実行の2工程が必要になります。
普通のPreparedStatementを使ったときの動きとほぼ同じです。

プリコンパイル

// 参照系
conn.prepare("select * from table where id = ?");

// 更新系
conn.prepare("insert into table values(?, ?, ?)");  

参照系SQLの実行

参照系SQLを実行すると、その結果をJavaBeanで受け取ることができます。 JavaBeanを定義していない場合などは、List<Map<String, String>>で受け取ることもできます。
executeQueryを使って対応する?に対するパラメータをセットします。

// JavaBeanを生成する
List<TestBean> result = conn.executeQuery(TestBean.class, param1, param2);

// Mapで受け取る
List<Map<String, String>> result = conn.executeQuery(param1, param2);

更新系SQLの実行

こちらはexecuteUpdateを使って、パラメータを指定します。

int count = conn.executeUpdate(3, 4, "value");  

トランザクション制御

自動コミットを切っていますので、更新系SQLを発行した後はコミットが必要になります。

try {  
    conn.executeUpdate("value");
    conn.commit();
}
catch (SQLException ex) {  
    conn.rollback();
}
finally {  
    conn.close();
}

JavaBeanの定義

参照結果をJavaBeanに変換するには、 JavaBeanの定義時にアノテーションでどの項目がどのカラムに対応するのかを定義する必要があります。
また、以下のJavaBean仕様に従う必要があります。

  • public で引数無しのコンストラクタが必要
    • コピーコンストラクタ等を定義したい場合は、引数無しのコンストラクタも合わせて定義する必要があります
  • メソッドの命名規則にしたがっていること(getter/setter)
public class TestBean {  
    @Column("test_id")
    private int testId;

    @Column("test_name")
    private String testName;

    public TestBean() {
    }

    public TestBean(TestBean testBean) {
        this.testId = testBean.testId;
        this.testName = testBean.testName;
    }

    public void setTestId(int testId) {
        this.testId = testId;
    }
    public int getTestId() {
        return this.testId;
    }

    public void setTestName(String testName) {
        this.testName = testName;
    }
    public String getTestName() {
        return this.testName;
    }
}

応用

JDBIというライブラリがあります。

JDBIでSELECT結果からJavaBeanを得るには自分でMapperを定義する必要があります。

public class UserBean implements Serializable {  
    private static final long serialVersionUID = 1L;
    @Column("id")
    private int id;
    @Column("username")
    private String username;
    @Column("password")
    private String password;

    public void setId(int id) {
        this.id = id;
    }
    public int getId() {
        return this.id;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getUsername() {
        return this.username;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getPassword() {
        return this.password;
    }
}

public interface UserDAO {

    /**
     * get user-info with id.
     *
     * @param id id
     * @return user-info
     */
    @SingleValueResult(UserBean.class)
    @SqlQuery(
              " select "
            + "   id, "
            + "   username, "
            + "   password "
            + " from "
            + "   user "
            + " where "
            + "   id = :id "
    )
    @Mapper(UserJdbiMapper.class)
    public Optional<UserBean> getUserById(@Bind("id") String id);
}

このようなBeanとDAOを定義した場合、SELECT結果のMapperはこんな感じになります。

public class UserJdbiMapper implements ResultSetMapper<UserBean> {

    @Override
    public UserBean map(int i, ResultSet rs, StatementContext ctx) throws SQLException {
        UserBean bean = new UserBean();
        bean.setId(rs.getInt("id"));
        bean.setUserName(rs.getString("username"));
        bean.setPassword(rs.getString("password"));
        return bean;
    }
}

DBUtils3を使うと以下のように定義することができます。
カラム数が多いJavaBeanを扱うときに多少楽になります。

public class UserJdbiMapper implements ResultSetMapper<UserBean> {  
    private BeanBuilder builder = new BeanBuilder(UserBean.class);

    @Override
    public UserBean map(int inex, ResultSet rs, StatementContext ctx) throws SQLExcetion {
        try {
            UserBean bean = builder.build(rs);
            return bean;
        }
        catch (BeanBuilderException ex) {
            throw new SQLException(ex);
        }
    }
}

しくみ

Mapper

JavaBeanのフィールドで定義したColumnの名前とフィールドに対するsetterの組み合わせを保持するクラスです。
setterMapper.AbstractSetterを継承したインスタンスです。 フィールドの型に応じた値をResultSetから取得し、フィールドにセットします。

このクラスはBeanBuilderを生成したタイミングで作成しています。

1つのJavaBeanに対して1回生成すればあとは使いまわせるため、BeanBuilderの生成はBeanBuilderFactoryにて管理するようにしています。 そのため2回目以降のBeanBuilderの生成はちょっとだけ早いです。

/// カラム名とsetterのマップ
private Map<String, AbstractSetter> mapper = new HashMap<>();

/*-
 * @Columnが指定されたフィールドを調べ、フィールドの型に応じたsetterをセットする
 */
for (Field field: cls.getDeclaredFields()) {  
    /*-
     * フィールドから@Columnアノテーションを取得する
     * アノテーションが取得できないフィールドはスキップ
     */
    final Column column = field.getAnnotation(Column.class);
    if (column == null) {
        continue;
    }

    try {
        /*-
         * フィールドからsetterメソッドを取得する
         * setterがないフィールドはスキップ
         * ! PropertyDescriptorを使っているためJavaの規約に準拠している必要があります
         */
        PropertyDescriptor pd = new PropertyDescriptor(field.getName(), cls);
        final Method setter = pd.getWriteMethod();
        if (setter == null) {
            continue;
        }

        AbstractSetter invoker = null;

        // 
        mapper.put(column.value(), invoker);

        if (field.getType().equals(boolean.class)) {
            invoker = new BooleanSetter(setter);
        }
        else if (field.getType().equals(byte.class)) {
            invoker = new ByteSetter(setter);
        }
        // ... 省略 ...
    }
    catch (IntrospectionException ex) {
    }
}


static abstract class AbstractSetter {  
    protected Method setter;
    protected AbstractSetter(Method setter) {
        this.setter = setter;
    }
    public abstract void invoke(Object beanInst, ResultSet rs, String columnName)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, SQLException;
}

static class BooleanSetter extends AbstractSetter {  
    public BooleanSetter(Method setter) {
        super(setter);
    }
    @Override
    public void invoke(Object beanInst, ResultSet rs, String columnName)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, SQLException {
        setter.invoke(beanInst, rs.getBoolean(columName);
    }
}

static class ByteSetter extends AbstractSetter {  
    public ByteSetter(Method setter) {
        super(setter);
    }
    @Override
    public void invoke(Object beanInst, ResultSet rs, String columnName)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, SQLException {
        setter.invoke(beanInst, rs.getByte(columName);
    }
}

BeanBuilder

ResultSetをもとにJavaBeanを生成するクラスです。 コンストラクタにClassを渡し、そのタイミングでMapperを生成しています。

private Class<?> cls;  
private Mapper mapper;

public BeanBuilder(Class<?> cls) {  
    this.cls = cls;
    mapper = new Mapper();
    mapper.createMapper(cls);
}

public <T> T build(ResultSet rs) throws BeanBuilderException {  
    try {
        @SuppressWarnings("unchecked")
        T bean = (T)cls.newInstance();

        /*-
         * Mapperで定義したsetterを使って、ResultSetから値を取得しBeanにセットする。
         *
         */
        for (Entry<String, AbstractSetter> entry: mapper.entrySet()) {
            try {
                AbstractSetter setter = entry.getValue();
                if (setter != null) {
                    setter.invoke(bean, rs, entry.getKey());
                }
            }
            catch (IllegalArgumentException | InvocationTargetException | SQLException ex) {
                // 何かしら例外が出たらそのフィールドはnullにする
            }
        }
        return bean;
    }
    catch (InstantiationException | IllegalAccessException ex) {
        throw new BeanBuilderException(ex);
    }
}

buildの宣言でGeneric Methodを使っています。 実際は以下のようにObject型で宣言するのとほとんど同じなのですが、 使う側がキャストしなくてすむため、Generic Methodで宣言しています。

// Generic Methodなしでの宣言
public Object build(ResultSet rs) BeanBuilderException {  
    Object bean = new Object();

    // ... 省略 ...

    return bean;
}

/// 使うとき

// Generic Methodあり
TestBean bean = beanbuilder.build(resultset);

// Generic Methodなし
TestBean bean = (TestBean)beanbuilder.build(resultset);  

BeanBuilderFactory

前述のとおり、MapperBeanBuilderは1つのJavaBeanに対して1回だけ作ればあとは使い回しが可能なため、 Factoryクラスを作ってそこで生成の管理を行っています。

具体的にはClassBeanBuilderのマップを作り、BeanBuilder生成の要求がきたら作成済みかどうかを確認しています。 複数のスレッドから同じJavaBeanに対する生成要求がきた場合はBeanBuilderが上書きされる可能性がありますが、 以下の理由であえて無視しています。

  • 上書きされても結局は同じ動きをするBeanBuilderがマップに登録されるため
  • synchronizedをつけると普段の動作も遅くなってしまう。レアパターンに対応するよりは普段の動作速度の方が重要であるため
private Map<Class<?>, BeanBuilder> mapper = new HashMap<>();


@Override
public <T> BeanBuilder getBeanBuilder(Class<T> cls) {  
    BeanBuilder builder = mapper.get(cls);
    if (builder == null) {
        builder = new BeanBuilder(cls);
        mapper.put(cls, builder);
    }
    return builder;
}

改善

BeanBuilderFactoryは生成要求があったタイミングでBeanBuilderを作っていますが、 起動時などにあらかじめ生成できると良いかなと思っています。