Mapperの自作
Motivation
JavaからカリカリにチューニングしたSQLをがしがし投げていたので、ORマッパー何それ状態でメリットがわからないままここまできてしまいました。(あとHibernateを使おうとして設定方法がよくわからずに諦めています)
SELECT結果をJavaBeansに突っ込むときは、毎回手でbean.setXXXX(result.getXXXX())と書くのがいやになってきたので、そこそこ簡単にSELECT結果をJavaBeansにしてくれるものを作ろうと思いました。
余談
いきなり余談ですが、DBとのインタフェースはこんな感じのラッパーを作っています。すべての結果をStringで返しているので、Integerで使いたい場合は毎回キャストしています。
DB.prepare("select column_a from tbl where key = ?");
List<Map<String, String>> result = DB.executeQuery("3");
String columnA = result.get(0).get("column_a");
いろいろと面倒だったのですべてStringにしていますが、PropertyDescriptorを使えばちゃんとsetterを使えるのでちゃんと作ろうと思えばちゃんと作れるはずです。
JavaBeansに入れるためにBeanBuilderを作る part 1
探せばいっぱい見つかると思いますが、何でも一度は作ってみます。
今回使うJavaBeansです。
class TblBean {
private String columnA;
public void setColumnA(String columnA) {
this.columnA = columnA;
}
public String getColumnA() {
return this.columnA;
}
}
まず作ってみたBeanBuilderです。
class BeanBuilder<T> {
public static class Mapper {
private Map<String, String> map;
public Mapper() {
this.map = new HashMap<>();
}
public void put(String key, String value) {
this.map.put(key, value);
}
public String get(String key) {
return this.map.get(key);
}
}
Mapper mapper;
public BeanBuilder(Mapper mapper) {
this.mapper = mapper;
}
public T build(T dst, Map<String, String> src) {
for (Map.Entry<String, String> entry: mapper.map.entrySet()) {
try {
PropertyDescriptor pd = new PropertyDescriptor(entry.getValue(), dst.getClass());
Method setter = pd.getWriteMethod();
setter.invoke(dst, src.get(entry.getKey()));
}
catch (IntrospectionException|IllegalAccessException|IllegalArgumentException|InvocationTargetException ex) {
// 設定に失敗した場合は何も設定しない
}
}
return bean;
}
}
使い方
class DAOImpl implements DAO {
private String selectSQL = "select column_a from TBL where id = ?";
private static final Mapper mapper;
static {
mapper = new Mapper();
mapper.put("column_a", "columnA");
}
public List<TblBean> select(String key) throws SQLException {
DB.prepare(selectSQL);
List<Map<String, String>> result = DB.executeQuery(key);
BeanBuilder builder = new BeanBuilder(mapper);
List<TblBean> list = new ArrayList<>();
for (Map<String, String> record: result) {
TblBean bean = new TblBean();
builder.build(bean, record);
list.add(bean)
}
return list;
}
}
SELECT時のカラム名とそれに対応するフィールド名をMapperに詰め込みます。
そのMapperを元にBeanBuilderを構築し、設定先のJavaBeansとSELECT結果からJavaBeansを作ります。
普通に使う分にはこんなんでも大丈夫です。
JavaBeansに入れるためにBeanBuilderを作るpart 2
part1での美しくない点
TblBean bean = new TblBean();
builder.build(bean, record);
Genericsを使っており、new T()とかができないので仕方なく外でインスタンスを作り、それを渡してもらっています。ちょっと恥ずかしいです。外でnewしなくてすむようにすると、かっこいい気がします。
ということで、外でnewしないバージョンを作ってみました。
class BeanBuilder<T> {
private static class Mapper {
private Map<String, String> map;
public Mapper() {
this.map = new HashMap<>();
}
public void put(String key, String value) {
this.map.put(key, value);
}
public String get(String key) {
this.map.get(key);
}
}
private Class<T> cls;
private Mapper mapper;
public BeanBuilder(Mapper mapper, T... t) {
this.cls = getCls(t);
this.mapper = mapper;
}
private Class<T> getCls(T... t) {
@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)t.getClass().getComponentType();
return cls;
}
private T createInstance() throws BeanBuilderException() {
try {
T inst = cls.newInstance();
return inst;
}
catch (InstantiationException|IllegalAccessException ex) {
throw new BeanBuilderException(ex);
}
}
public T build(Map<String, String> src) throws BeanBuilderException {
T dst = createInstance();
for (Map.Entry<String, String> entry: mapper.map.entrySet()) {
try {
PropertyDescriptor pd = new PropertyDescriptor(entry.getValue(), dst.getClass());
Method setter = pd.getWriteMethod();
setter.invoke(dst, src.get(entry.getKey()));
}
catch (IntrospectionException|IllegalAccessException|IllegalArgumentException|InvocationTargetException ex) {
// 設定に失敗した場合は何も設定しない
}
}
return dst;
}
}
キモはgetCls()です。
private Class<T> getCls(T... t) {
@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)t.getClass().getComponentType();
return cls;
}
Class<T> cls = getCls();
とやると、tはサイズ0の配列となるため、tに対してgetClass()が使えます。それをもとにClassを取得し、ClassからnewInstance()を実行しています。
何回か試したのですが、外部から呼ばれないと型推論ができないようで作成されたClass<T>がClass<Object>になってしまうことがありました。そのため、コンストラクタに余計なものをつけています。
使い方
class DAOImpl implements DAO {
private String selectSQL = "select column_a from TBL where id = ?";
private static final Mapper mapper;
static {
mapper = new Mapper();
mapper.put("column_A", "columnA");
}
public List<TblBean> select(String key) throws SQLException, BeanBuilderException {
DB.prepare(selectSQL);
List<Map<String, String>> result = DB.executeQuery(key);
BeanBuilder<TblBean> builder = new BeanBuilder<>(mapper);
List<TblBean> list = new ArrayList<>();
for (Map<String, String> record: result) {
TblBean bean = builder.build(record);
list.add(bean)
}
return list;
}
}
コンストラクタの引数は2つですが、可変長配列なので今までどおり1つ渡せば大丈夫です。ただ、IDEを使っていると引数の候補として表示されるので、そこが結構かっこ悪いです。
JavaBeansに入れるためにBeanBuilderを作る part3
JavaBeans側にアノテーションでDBの期待するカラム名を入れておけば、Mapperを作らなくても良いのでは?
手間はあまり変わりませんが、せっかくなのでやってみます。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
String value();
}
class TblBean {
@Column("column_a")
privte String columnA;
public void setColumnA(String columnA) {
this.columnA = columnA;
}
public String getColumnA() {
return this.columnA;
}
}
public class BeanBuilder<T> {
private Class<T> cls;
private Mapper mapper;
// Mapperを渡さないversion
public BeanBuilder(T... t) {
this.cls = getCls(t);
this.mapper = createMapper(this.cls);
}
// Mapperを渡す
public BeanBuilder(Mapper mapper, T... t) {
this.cls = getCls(t);
this.mapper = mapper;
}
public Class<T> getCls(T... t) {
@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)t.getClass().getComponentType();
return cls;
}
private Mapper createMapper(Class<T> cls) {
Mapper mapper = new Mapper();
Field[] fields = cls.getDeclaredFields();
for (Field field: fields) {
if (isColumn(field)) {
Column c = field.getAnnotation(Column.class);
mapper.put(c.value(), field.getName());
}
}
return mapper;
}
private T createInstance() throws BeanBuilderException() {
try {
T inst = cls.newInstance();
return inst;
}
catch (InstantiationException|IllegalAccessException ex) {
throw new BeanBuilderException(ex);
}
}
public T build(Map<String, String> src) throws Exception {
T dst = createInstance();
for (Map.Entry<String, String> entry: mapper.map.entrySet()) {
try {
PropertyDescriptor pd = new PropertyDescriptor(entry.getValue(), dst.getClass());
Method setter = pd.getWriteMethod();
setter.invoke(dst, src.get(entry.getKey()));
}
catch (IntrospectionException|IllegalAccessException|IllegalArgumentException|InvocationTargetException ex) {
// 設定に失敗した場合は何も設定しない
}
}
return dst;
}
private boolean isColumn(Field field) {
return field.getAnnotation(Column.class) != null;
}
Mapperを渡さないコンストラクタを使った場合、JavaBeansのフィールドに@ColumnがあればそこからMapperを自動で作成して、Builderを構成してくれます。
使い方
class TblBean {
@Column("column_a")
private String columnA;
public void setColumnA(String columnA) {
this.columnA = columnA;
}
public String getColumnA() {
return this.columnA;
}
}
class DAOImpl implements DAO {
private String selectSQL = "select column_a from TBL where id = ?";
public List<TblBean> select(String key) throws SQLException, BeanBuilderException {
DB.prepare(selectSQL);
List<Map<String, String>> result = DB.executeQuery(key);
BeanBuilder<TblBean> builder = new BeanBuilder<>(mapper);
List<TblBean> list = new ArrayList<>();
for (Map<String, String> record: result) {
TblBean bean = builder.build(record);
list.add(bean)
}
return list;
}
}
この形式にすると、DAOとJavaBeansはコンパイラ上は疎結合ですが、プログラマの頭の中では密に結合しないと「SELECTした結果がJavaBeansに入っていない」ということが起きがちです。SQLをカリカリ自分で書いて、どのJavaBeansにMapさせるかなどを全部把握していないと、効率が悪くなるので注意が必要です。
発展
JavaBeansのフィールド名とDBが一致しているなら、DBのカラム名からsetterを自動判別してくれるとさらに助かりますね。