DBUtils – BeanProcessor扩展,支持自定义字段映射

原文出处:http://www.showcoo.net/56.html

ApacheCommos的DbUtils是一个简单好用的轻量级的数据库操作工具,该项目的主页是:http://commons.apache.org/dbutils/,关于它的信息可以从那里获取.

在进行查询自动类映射的时候domain类的属性必须和数据库中名称一样,但是如果数据库的表名使用带下划线的设计,

domain类中的属性也要使用下划线,看起来很不雅观,而且使用hibernate工具自动生成的domain类会自动去掉下划线,

下划线后面的用大写字母,比如user_name转换后为userName,因此有必要让DbUtils支持这种设计。

(注:如果不像扩展代码,请往下拉进行参照第二种方法进行匹配)

方法一:

从以上代码可看出dbutils可以把查询出来的结果集映射成Bean的List,这是个很有用的功能,不过有一个很大限制,规定数据库字段名必须跟Bean的属性名一致,例如字段名为”my_name”,那么属性名也必须为”my_name”.
这个限制的影响还是比较大的,这样好像BeanListHandler就没有什么作用了?而我希望的是该工具可以定制各种各样的结果集与Bean的映射规则.比如,可以使用”驼峰规则”来匹配,字段”MY_NAME”对应的属性为”myName”,或者是直接指定一个二维数组定义字段与属性之间的对应关系.
说到这里,我们的扩展目标也出来.现在,看一下dbutils的源代码是否有这方面的扩展点.先看一下BeanListHandler的相关代码.

...
public class BeanListHandler implements ResultSetHandler> {

    /**
     * The Class of beans produced by this handler.
     */
    private final Class type;

    /**
     * The RowProcessor implementation to use when converting rows
     * into beans.
     */
    private final RowProcessor convert;

    public BeanListHandler(Class type) {
        this(type, ArrayHandler.ROW_PROCESSOR);
    }

    public BeanListHandler(Class type, RowProcessor convert) {
        this.type = type;
        this.convert = convert;
    }

    public List handle(ResultSet rs) throws SQLException {
        return this.convert.toBeanList(rs, type);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
. . .
public class BeanListHandler < T > implements ResultSetHandler < List < T >> {
    /**
     * The Class of beans produced by this handler.
     */
    private final Class < T > type ;
    /**
     * The RowProcessor implementation to use when converting rows
     * into beans.
     */
    private final RowProcessor convert ;
    public BeanListHandler ( Class < T > type ) {
        this ( type , ArrayHandler . ROW_PROCESSOR ) ;
    }
    public BeanListHandler ( Class < T > type , RowProcessor convert ) {
        this . type = type ;
        this . convert = convert ;
    }
    public List < T > handle ( ResultSet rs ) throws SQLException {
        return this . convert . toBeanList ( rs , type ) ;
    }
}

根据上面的代码片断可以看出,BeanListHandler的功能是由convert(RowProcessor)提供的,而convert的默认实现为ArrayHandler.ROW_PROCESSOR.再看一下ArrayHandler.ROW_PROCESSOR的相关代码.

public class ArrayHandler implements ResultSetHandler {

    ...

    static final RowProcessor ROW_PROCESSOR = new BasicRowProcessor();

    ...
}

1
2
3
4
5
6
7
8
public class ArrayHandler implements ResultSetHandler < Object [ ] > {
    . . .
    static final RowProcessor ROW_PROCESSOR = new BasicRowProcessor ( ) ;
    . . .
}

很明显BeanListHandler是依赖BasicRowProcessor来完成相应的功能的.不过,这不是终点,继续看一下,会发现,真实完成字段与属性映射功能的是类BeanProcessor,再看一下BeanProcessor的源码.

...
public class BeanProcessor {

    ...

    public List toBeanList(ResultSet rs, Class type) throws SQLException {
        List results = new ArrayList();

        if (!rs.next()) {
            return results;
        }

        PropertyDescriptor[] props = this.propertyDescriptors(type);
        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

        do {
            results.add(this.createBean(rs, type, props, columnToProperty));
        } while (rs.next());

        return results;
    }

    ...

    protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
            PropertyDescriptor[] props) throws SQLException {

        int cols = rsmd.getColumnCount();
        int columnToProperty[] = new int[cols + 1];
        Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

        for (int col = 1; col <= cols; col++) {
            String columnName = rsmd.getColumnLabel(col);
            if (null == columnName || 0 == columnName.length()) {
              columnName = rsmd.getColumnName(col);
            }
            for (int i = 0; i < props.length; i++) {

                if (columnName.equalsIgnoreCase(props[i].getName())) {
                    columnToProperty[col] = i;
                    break;
                }
            }
        }

        return columnToProperty;
    }

    ...
}

阅读以上代码可知,方法toBeanList提供将结果集映射成包括Bean的List的功能,方法内部使用数组columnToProperty来存放各字段对应属性的索引位置,

而此数组是由方法mapColumnsToProperties来提供的,而且此方法是通过columnName.equalsIgnoreCase(props[i].getName())来确定指定字段对应的属性的,

就是这里造成了上面提到的限制.那么很明显这里就是要找的扩展点了.只要重写此方法就可以实现了,比如文章开头提到的各种需求.不过我觉得还有更好的扩展方法,

例如可以把匹配字段名与属性名这个逻辑抽象出来作为一种策略,并且提供常用的策略以供选择,而且有新的需求同样可以通过提供新的策略来实现.

这样做还有另一个好处,就是把扩展时的关注点范围缩小,集中到”匹配”这一点上来,而不去关心其它问题.这样的好处可想而知,可以很容易的写出实现与明确的单元测试.

以下为我的扩展的实现,首先是MyBeanProcessor:

import java.beans.PropertyDescriptor;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.Arrays;

import org.apache.commons.dbutils.BeanProcessor;

/**
 * 策略模式的BeanProcessor
 */
public class MyBeanProcessor extends BeanProcessor {

    private Matcher matcher;

    public MyBeanProcessor(){
        // 默认Matcher
        matcher = new HumpMatcher();
    }

    public MyBeanProcessor(Matcher matcher){
        this.matcher = matcher;
    }

    public Matcher getMatcher() {
        return matcher;
    }

    public void setMatcher(Matcher matcher) {
        this.matcher = matcher;
    }

    /**
     * 重写BeanProcessor的实现,使用策略模式
     */
    protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
            PropertyDescriptor[] props) throws SQLException {
        if (matcher == null)
            throw new IllegalStateException("Matcher must be setted!");

        int cols = rsmd.getColumnCount();
        int columnToProperty[] = new int[cols + 1];
        Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

        for (int col = 1; col <= cols; col++) {
            String columnName = rsmd.getColumnLabel(col);
            if (null == columnName || 0 == columnName.length()) {
              columnName = rsmd.getColumnName(col);
            }
            for (int i = 0; i < props.length; i++) {
                if (matcher.match(columnName, props[i].getName())) {//与BeanProcessor不同的地方
                    columnToProperty[col] = i;
                    break;
                }
            }
        }

        return columnToProperty;
    }
}

 

MyBeanProcessor重写了BeanProcessor的mapColumnsToProperties方法,把原先写死的字段名与属性名的匹配逻辑交由Matcher来实现.Matcher是一个接口,它是”字段名与属性名是否匹配”的抽象.

继续,下面是接口Matcher:

public interface Matcher {

    /**
     * 判断字段名与属性名是否匹配
     *
     * @param columnName 字段名
     * @param propertyName 属性名
     * @return 匹配结果
     */
    boolean match(String columnName, String propertyName);
}

接口Matcher非常简单,只有一个方法match.再看一下两个常用实现,分别是MappingMatcher(二维数组匹配)与HumpMatcher(驼峰命名匹配):

import java.util.HashMap;
import java.util.Map;

/**
 * 二维数组映射的匹配器
 *
 */
public class MappingMatcher implements Matcher {

    private Map _map = null;

    public MappingMatcher(String[][] mapping) {
        if (mapping == null)
            throw new IllegalArgumentException();

        _map = new HashMap();
        for (int i = 0; i < mapping.length; i++) {
            String columnName = mapping[i][0];
            if (columnName != null)
                _map.put(columnName.toUpperCase(), mapping[i][1]);
        }
    }

    public boolean match(String columnName, String propertyName) {
        if (columnName == null)
            return false;
        String pname = _map.get(columnName.toUpperCase());
        if (pname == null)
            return false;
        else {
            return pname.equals(propertyName);
        }
    }
}

 

/**
 * 驼峰转换的匹配器
 *
 */
public class HumpMatcher implements Matcher {

    public boolean match(String columnName, String propertyName) {
        if (columnName == null)
            return false;

        columnName = columnName.toLowerCase();
        String[] _ary = columnName.split("_");
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < _ary.length; i++) {
            String str = _ary[i];
            if (!"".equals(str) && i > 0) {
                StringBuilder _builder = new StringBuilder();
                str = _builder.append(str.substring(0, 1).toUpperCase()).append(str.substring(1)).toString();
            }
            strBuilder.append(str);
        }
        return strBuilder.toString().equals(propertyName);
    }

}

有了这样的扩展,以后就可以这样写代码了:

QueryRunner run = new QueryRunner(dataSource);

ResultSetHandler> h = new BeanListHandler(Person.class, new BasicRowProcessor(new MyBeanProcessor(new HumpMatcher())));

List persons = run.query("SELECT * FROM Person", h);

1
2
3
4
5
QueryRunner run = new QueryRunner ( dataSource ) ;
ResultSetHandler < List > h = new BeanListHandler ( Person . class , new BasicRowProcessor ( new MyBeanProcessor ( new HumpMatcher ( ) ) ) ) ;
List persons = run . query ( "SELECT * FROM Person" , h ) ;

方法二:

参照BeanProcessor源码,

...  
public class BeanProcessor {  
    ...  
      protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,  
            PropertyDescriptor[] props) throws SQLException {  
          ...

        for (int col = 1; col <= cols; col++) {  
            String columnName = rsmd.getColumnLabel(col);  
            ...
        }  
        ...
    }  
    ...  
}

因为字段名是通过方法rsmd.getColumnLabel(col)来取得的,所以还可以把字段名与属性名的映射关系写在SQL语句里面,如:

SELECT ID as id, MY_NAME as myName FROM MY_TABLE

这样用不到在进行扩展了.直接用列的别名进行匹配,

你可能感兴趣的:(java)