DbUtils优化

DbUtils 是一个非常轻巧的操作数据库的工具,我们在之前的文章中也有介绍,使用它可以非常方便地对数据库进行操作,特别是查询时,这个工具提供了很多对结果集的封装,因此可以将查询数据库得到的结果集封装成 Java 对象,操作起来十分方便,现在在使用过程中,发现当数据表中的字段和我们 Java 类中定义的字段名称不一致时,封装成 Java 对象时,对象中对应的属性值是没有的,也就是说没有将数据表中查取出来的数据成功封装到 Java 对象上面,这也是我们这篇文章想要解决的问题。

1.问题描述

在使用 DbUtils 查询数据库时,我们一般的写法会是这样的。

String sql = "select * from person limit 1";
QueryRunner queryRunner = new QueryRunner();
List<Person> personList = queryRunner.query(conn, sql, new BeanListHandler<Person>(Person.class));

这样查询时,DbUtils 就会自动帮我们将从数据表查出来的结果集对象封装为 Java 对象,但是这样封装成功是有前提的,前提就是数据表中字段的名称和 Java 类中定义的名称是一致的。

那如果是这种情况呢?数据表如下:

CREATE TABLE `person` (
    `id` BIGINT (32) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `order_id` VARCHAR (32) DEFAULT NULL,
    `user_account_type` VARCHAR (3) DEFAULT NULL,
    `user_account` VARCHAR (128) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 9074 DEFAULT CHARSET = utf8 COMMENT = '用户表';

Java 类如下:

public class Person {

    private Long id;

    private String orderId;

    private String userAccountType;

    private String userAccount;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getUserAccountType() {
        return userAccountType;
    }

    public void setUserAccountType(String userAccountType) {
        this.userAccountType = userAccountType;
    }

    public String getUserAccount() {
        return userAccount;
    }

    public void setUserAccount(String userAccount) {
        this.userAccount = userAccount;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", orderId=" + orderId + ", userAccountType=" + userAccountType + ", userAccount="
                + userAccount + "]";
    }
}

很明显在这种情况下,数据表中的字段和 Java 类中的字段,它们的字段名称是不一致的,那这种情况我们应该如何处理,而且要保证数据映射成功呢?我们下面来介绍。

2.第一种方法

首先说明下我们使用的 DbUtils 版本为 1.7,后续有对 DbUtils 进行代码扩展也是在该版本进行扩展的。

这里我们先说第一种方法,第一种方法便是使用别名的方式,使用方式如下:

String sql = "select id, order_id AS orderId, user_account_type AS userAccountType, user_account AS userAccount from person limit 1";
QueryRunner queryRunner = new QueryRunner();
List<Person> personList = queryRunner.query(conn, sql, new BeanListHandler<Person>(Person.class));

也就是在写 SQL 语句时,使用 as 关键字来完成数据表字段到 Java 类字段的映射。这种方法如果字段较少时,使用还是很方便的,但是当字段名称不同的很多时,修改 SQL 还是比较费劲的。

3.第二种方法

我们首先需要看看在使用 QueryRunner 类调用 query() 方法查询时,执行的流程到底是怎样的,该 query() 方法实际执行的代码为:

private <T> T query(Connection conn, boolean closeConn, String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException {
    if (conn == null) {
        throw new SQLException("Null connection");
    }

    if (sql == null) {
        if (closeConn) {
            close(conn);
        }
        throw new SQLException("Null SQL statement");
    }

    if (rsh == null) {
        if (closeConn) {
            close(conn);
        }
        throw new SQLException("Null ResultSetHandler");
    }

    PreparedStatement stmt = null;
    ResultSet rs = null;
    T result = null;

    try {
        stmt = this.prepareStatement(conn, sql);
        this.fillStatement(stmt, params);
        rs = this.wrap(stmt.executeQuery());
        result = rsh.handle(rs);
    } catch (SQLException e) {
        this.rethrow(e, sql, params);
    } finally {
        try {
            close(rs);
        } finally {
            close(stmt);
            if (closeConn) {
                close(conn);
            }
        }
    }
    return result;
}

这次我们需要关注的代码主要是将查询得到的结果集封装为 Java 对象的部分,也就是这行代码:

result = rsh.handle(rs);

这里其实是 ResultSetHandler 接口中定义的一个封装方法,这个接口有很多实现类,其中一个就是 BeanListHandler,这个类我们很熟悉了,就是我们之前经常使用的,将数据结果集封装为一个 Java 中的 List 对象,它其中的实现为:

@Override
public List<T> handle(ResultSet rs) throws SQLException {
    return this.convert.toBeanList(rs, type);
}

很显然它也进一步调用的类中 convert 对象中的 toBeanList() 方法完成的封装,而这个 convert 对象其实是在 BeanListHandler 类中声明的一个成员属性:

private final RowProcessor convert;

我们接着跳转进入上面的 this.convert.toBeanList() 方法中,会发现进入到了 BasicRowProcessor 类中的 toBeanList() 方法,方法内容为下:

@Override
public <T> List<T> toBeanList(ResultSet rs, Class<? extends T> type) throws SQLException {
    return this.convert.toBeanList(rs, type);
}

为什么会调转到 BasicRowProcessor 类中的 toBeanList() 方法呢?原来 RowProcessor 只是一个接口,而它有一个实现类就是 BasicRowProcessor,所以才会跳转到这里。那我们接着看 BasicRowProcessor 类中的 toBeanList() 方法,发现它调用的是 BeanProcessor 类中的 toBeanList() 方法,具体内容为下:

public <T> List<T> toBeanList(ResultSet rs, Class<? extends T> type) throws SQLException {
    List<T> results = new ArrayList<T>();

    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;
}

为什么会调转到这里呢?这是因为在 BasicRowProcessor 类中的 toBeanList() 方法里面的 this.convert 对象引用的其实是类中声明的一个成员属性 BeanProcessor

private final BeanProcessor convert;

好了,我们接着看 BeanProcessor 类中的 toBeanList() 方法,这个方法中我们需要关注的就是数据表字段和 Java 类属性映射的部分,也就是下面这个代码:

int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

我们直接跳转到这个 mapColumnsToProperties() 方法,内容为下:

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);
        }
        String propertyName = columnToPropertyOverrides.get(columnName);
        if (propertyName == null) {
            propertyName = columnName;
        }
        for (int i = 0; i < props.length; i++) {

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

    return columnToProperty;
}

看上面这段代码,是在其中定义的一个数组 columnToProperty 保存的数据表字段到 Java 类字段的映射,也就是根据这个映射,后面才能将数据表中查出来的结果映射到 Java 类中,还需要说明一下的是,上面方法中的 columnToPropertyOverrides,这个 Map 集合对象,保存的是数据表字段到 Java 类字段名称的映射,现在这个 Map 集合是空的,因此只有当数据表字段名和 Java 类中字段名称一样时,才能找到对应的映射索引,如果没有找到的话,columnToProperty 数组保存的映射索引位置其对应的值就还是 -1,以表示 Java 类中没有和数据表该字段相映射的字段,这也就是当前 DbUtils 只支持数据表字段和 Java 类字段完全一样的情况的原因。那如果我们为这个 columnToPropertyOverrides 存入数据表字段和 Java 类字段名称对应好的映射呢?那不就是可以实现我们想要的功能了。

我们先理一下这个流程,QueryRunner 类在查询得到结果集对象后,是使用的 ResultSetHandler 接口的实现类 BeanListHandler 类来实现到 Java 对象的转换的,而 BeanListHandler 类则是调用它其中的成员属性 RowProcessor 接口的实现类 BasicRowProcessor 来完成转换的,而 BasicRowProcessor 类则是调用它其中的成员属性 BeanProcessor 类完成转换的。也就是在 BeanProcessor 类的 mapColumnsToProperties() 方法中我们得到了数据表字段和 Java 类字段的映射关系,那这时候,如果我们新建一个类,继承 BeanProcessor 类,重写其中的 mapColumnsToProperties() 方法,使这个方法可以返回我们想要的映射关系,然后再将这个类传入到 BasicRowProcessor 类中,使得 BasicRowProcessor 类中的成员属性 BeanProcessor 引用拿到的是我们自己建的类,然后再将 BasicRowProcessor 类传入到 BeanListHandler 类中,最后将 BeanListHandler 类传入到 QueryRunner 类的 query() 方法中,我们就能够实现目的了。

先看我们写的 BeanProcessor 类的实现类:

public class ColumnBeanProcessor extends BeanProcessor {

    private final Map<String, String> columnToPropertyOverrides;

    public ColumnBeanProcessor(Map<String, String> columnToPropertyOverrides) {
        super();
        this.columnToPropertyOverrides = columnToPropertyOverrides;
    }

    @Override
    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);
            }
            String propertyName = columnToPropertyOverrides.get(columnName);
            if (propertyName == null) {
                propertyName = columnName;
            }
            for (int i = 0; i < props.length; i++) {
                if (propertyName.equalsIgnoreCase(props[i].getName())) {
                    columnToProperty[col] = i;
                    break;
                }
            }
        }
        return columnToProperty;
    }
}

看上去和之前的 BeanProcessor 类没有区别,这里最大的不同就是新加了一个成员属性columnToPropertyOverrides,而且允许在新建该类的时候传入对该属性的引用,之前我们已经说过,columnToPropertyOverrides 集合中应该保存的是数据表字段名称到 Java 类字段名称的对应关系,那我们怎么得到这个对应关系呢?我这里想到的办法是,新建一个注解用于标识数据表字段名称,然后将该注解用到 Java 类中的每个成员属性上面,这样我们就能通过反射拿到它们之间的对应关系了。

注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ColumnName {

    String value();
}

Java 类:

public class Person {

    @ColumnName("id")
    private Long id;

    @ColumnName("order_id")
    private String orderId;

    @ColumnName("user_account_type")
    private String userAccountType;

    @ColumnName("user_account")
    private String userAccount;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getUserAccountType() {
        return userAccountType;
    }

    public void setUserAccountType(String userAccountType) {
        this.userAccountType = userAccountType;
    }

    public String getUserAccount() {
        return userAccount;
    }

    public void setUserAccount(String userAccount) {
        this.userAccount = userAccount;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", orderId=" + orderId + ", userAccountType=" + userAccountType + ", userAccount="
                + userAccount + "]";
    }
}

获取数据表字段名和 Java 类字段名的对应关系:

public class ColumnBeanProcessorUtils {

    public static <T> Map<String, String> getColumnNameMap(Class<T> clazz) {
        Map<String, String> columnNameMap = new HashMap<>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            ColumnName columnName = field.getAnnotation(ColumnName.class);
            if (null != columnName) {
                columnNameMap.put(columnName.value(), field.getName());
            } else {
                columnNameMap.put(field.getName(), field.getName());
            }
        }
        return columnNameMap;
    }
}

这样我们就能完成我们想要的效果了,为了后续使用方便,我们可以将查询部分也抽取公共方法。

public final class QueryUtils {

    private static QueryRunner queryRunner = new QueryRunner();

    public static <T> List<T> selectBeanList(Connection conn, String sql, Class<T> type, Object[] params)
            throws Exception {
        System.out.println("select sql:[" + sql + "]");
        Map<String, String> columnNameMap = ColumnBeanProcessorUtils.getColumnNameMap(type);
        ColumnBeanProcessor convert = new ColumnBeanProcessor(columnNameMap);
        RowProcessor rp = new BasicRowProcessor(convert);
        ResultSetHandler<List<T>> bh = new BeanListHandler<T>(type, rp);
        List<T> list = queryRunner.query(conn, sql, bh, params);
        return list;
    }

    public static <T> List<T> selectBeanList(Connection conn, String sql, Class<T> type) throws Exception {
        return selectBeanList(conn, sql, type, null);
    }
}

4.总结

这次扩展主要是为了解决实际使用中的一些问题,真的能实现感觉还是很开心的。还是像这样慢慢地积累吧,通过积累让自己的能力一步一步地提升。

坚持原创技术分享,您的支持将鼓励我继续创作!