tamuraです。
Javaでデータベースを使う場合、以下の2パターンが考えられます。
- ORMを使ってアクセスする
- 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の組み合わせを保持するクラスです。
setterはMapper.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
前述のとおり、Mapper
やBeanBuilder
は1つのJavaBeanに対して1回だけ作ればあとは使い回しが可能なため、 Factory
クラスを作ってそこで生成の管理を行っています。
具体的にはClass
とBeanBuilder
のマップを作り、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
を作っていますが、 起動時などにあらかじめ生成できると良いかなと思っています。