最近工作关系需要对dbutils进行一些了解,来完善公司的测试DAO的框架封装。给本屌丝最大的感慨 麻雀虽小,五脏俱全。
dbutils是Apache组织开发的一个简单,小巧,又具有大部分功能的操作数据库的java组件,dbutis包含三个包,10几个类,够简单的吧!
使用dbutils也非常的简单,只需要初始化QueryRunner类,传入SQL语句,就可以进行增删改查的所有的操作,当然dbutils对查询也做了一些比较简单的查询的封装,主要是对ResultHandle做了一些处理。最终还是通过QueryRunner来进行的操作的。
QueryRunner可以直接new进行实例化,也有传入dataSource进行实例化的构造器,前者进行实例化之后,操作时需要传递Connnection参数,方便比较容易获得Connection的应用使用,而使用DataSource进行实例化就可以直接进行数据库操作了。这里特别的提醒一下。如果应用中存在多个数据库源,并且使用了proxool的数据库连接池,那必须设置数据库连接池的别名alias,这是因为数据库连接池在获取连接的时候首先通过别名来获得链接,如果都没有设置别名,将有可能获得错误的数据库连接,后果就严重了!大家可以看看这段代码就明了了。
ConnectionPool cp = null;
try {
if (!ConnectionPoolManager.getInstance().isPoolExists(alias)) {
registerPool();
}
cp = ConnectionPoolManager.getInstance().getConnectionPool(alias);
return cp.getConnection();
} catch (ProxoolException e) {
LOG.error("Problem getting connection", e);
throw new SQLException(e.toString());
}
如果应用中已经集成了spring,那将dbutils集成进来就非常的简单了,直接通过spring对queryRunner进行管理,这里就不细说了!会用spring的人都知道该怎么做了。至于增删改这些东西都直接通过SQL语句,比较直观,再封装也省不了多少工作量,所以今天主要介绍一下关于将resultSet转化成javaBean方面的转换。
实际上,dbutils对一些简单的resultSet-->Bean是有做一个处理器的,主要通过BeanProcessor来对resultSet到Bean的转换,如果不涉及到一些诸如Calendar,Enum,自定义特殊的类型或者说不涉及到一些关联的话,简单的javaBean是可以满足需求的。代码也不复杂:
BeanProcessor beanProcessor = new BeanProcessor();
RowProcessor rowProcessor = new BasicRowProcessor(beanProcessor);
BeanHandler handle = new BeanHandler(beanClass, rowProcessor);
//进行查询
T t = queryRunner.query(sql, handle);
这几行代码应该比较容易看懂了,很简单的几行代码就可以进行Bean的转换了,除了bean的转化,还有toBeanList,toBeanMap的转换,这些都可以轻松的做到。
但是往往需求就不是那么简单的, 我们的实体类是hibernate的实体类,里面进行一些实体之间的关联,同时还有我们对枚举类型进行了一些特殊的封装,使hibernate保存在数据库中的数据是我们自定义的一个数字或者是一个标识符,这个大家都肯定都能想到,这些特殊的类型无法直接从数据库保存的值直接转化成我们自己的类型,需要我们对这种特殊类型进行一些改造。在进行改造的时候,我们应该先知道怎么从columnToPerporty这样的一个过程。
首先,我们知道hibernate对实体的映射是通过反射来实例化实体类,获取属性来设置值的,dbutils也是一样,他通过一个默认的规则使得列名和属性名对应,然后找到需要设置值的属性名,当在实体中没有找到相关的属性时,就不设置,这样首先要解决的问题是,怎么把他的属性映射的默认规则改成我们自己的规则。对于这种属性和列对应的处理我们通过查阅BeanProcessor的源码来看默认的映射规则。
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;
}
找到这个方法,这个方法我一开始看的时候感觉怪怪的,怎么是个嵌套循环呢,不知道当时作者是怎么想的,我想了想,这样的嵌套循环最多的次数可能为 cols.length * cols.length,当然对于计算机来说这也许算不了什么,但是这样的写法的可读性毕竟是不太好的,所以我在改造的时候对这段代码进行重载,再来看看这段代码里面的关键的一句,让我们知道dbutils的列与属性的对应的默认的规则:columnName.equalsIgnoreCase(props[i].getName(),将列名与属性名称忽略大小写进行比较,如果比较相等,就对应上了。
麻烦来了,这样的对应关系跟我们的系统的hibernate的映射规则不匹配,在实体中我们使用驼峰表达式,而在数据库中我们遵循驼峰使用_来分开,还有更麻烦的是我们的hibernate中的映射不全遵循驼峰表达式(例如:phoneOrTel-->phone_or_tel),还有一些关联的实体是直接通过注解写在属性上方来进行列名映射的。如果这个映射规则不改变,那我们就无法使用BeanProcessor来进行记录集与bean的直接转换了。或许可以通过一个结果处理器重新来写,但是这样的话每张表都需要写ResultHandler来处理,而且需要一个
列一列进行转换,这样产生出来的工作量非常大,而且就算是全部映射完了,也不好去维护。于是考虑对BeanProcessor的方法进行覆盖重写,来满足我们映射规则上
的需求,
@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);
Map properMap = new HashMap();
for (int i = 0; i < props.length; i++) {
properMap.put(props[i].getName(), i);
}
for (int col = 1; col <= cols; col++) {
String columnName = rsmd.getColumnLabel(col);
if (null == columnName || 0 == columnName.length()) {
columnName = rsmd.getColumnName(col);
}
columnName = convert(columnName);
Integer descriptorIndex = properMap.get(columnName);
if (descriptorIndex != null) {
columnToProperty[col] = descriptorIndex.intValue();
}
}
return columnToProperty;
}
这段代码首先是对嵌套循环做了一个改变,然后是对映射的规则进行了重写,主要的逻辑在convert这个方法里面,主要代码如下所示。
private String convert(String columnName) {
columnName = columnName.toLowerCase();
//先从MAP中找出对应的属性值
XmlBeanInfo columnToPerporty = this.tableColumnMap.get(columnName);
if (columnToPerporty != null) {
return columnToPerporty.getProperty();
}
String regexValue = "(_\\w)\\w?";
Pattern pattern = Pattern.compile(regexValue);
Matcher matcher = pattern.matcher(columnName);
while (matcher.find()) {
String value = matcher.group(1);
String upCaseValue = value.replaceAll("_", "").toUpperCase();
columnName = columnName.replaceFirst(value, upCaseValue);
}
// 替换成驼峰表达式
return columnName;
}
这样我们就完成了对映射规则的改变了,至于实体之间关联使用注解在属性上方进行自定义映射来进行匹配的,这个等我有时间的时候再写个专题来介绍,也不复杂。
随后我们要解决的问题是关于实体中的一些特殊的类型,在dbutils中没有实现的,比如java.sql.date-->calendar类型的转换啊,如果不转化,值将无法反射进去,我们先来看看在dbutils中是怎么通过反射将值设置进入实体的。请看下面的代码
private T createBean(ResultSet rs, Class type,
PropertyDescriptor[] props, int[] columnToProperty)
throws SQLException {
T bean = this.newInstance(type);
for (int i = 1; i < columnToProperty.length; i++) {
if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
continue;
}
PropertyDescriptor prop = props[columnToProperty[i]];
Class> propType = prop.getPropertyType();
Object value = this.processColumn(rs, i, propType);
if (propType != null && value == null && propType.isPrimitive()) {
value = primitiveDefaults.get(propType);
}
this.callSetter(bean, prop, value);
}
return bean;
}
这段代码告诉我们,对每个列进行类型的转化之后设置进入实体属性中,这个方法是private方法,是不能被重写的,而processColumn这个方法是可以被重写的,我们对这个方法进行一些改造,就能满足我们的要求了,改造之后的代码如下:
@Override
protected Object processColumn(ResultSet rs, int index, Class> propType) throws SQLException {
Set> classSet = specialClassMap.keySet();
boolean containsInterface = classSet.contains(propType);
Class> containsClass = null;
if (!containsInterface) {
Class>[] allInterfaceArr = propType.getInterfaces();
for (Class> inte : allInterfaceArr) {
if (classSet.contains(inte)) {
containsInterface = true;
containsClass = inte;
}
}
} else {
containsClass = propType;
}
if (containsInterface) {
SpecialTypeHandle> handle = specialClassMap.get(containsClass);
if (handle == null) {
throw new IllegalArgumentException("The SpecialTypeHandle Cannot Be Null");
}
if (!propType.isPrimitive() && rs.getObject(index) == null) {
return null;
}
Object o = rs.getObject(index);
Object result = handle.getResultObject(propType,o);
return result;
}
return super.processColumn(rs, index, propType);
}
这段代码中涉及到的specialClassMap是在子类中注入的属性,这个属性就是我们定义的特殊的类的name与SpecialTypeHandle的键值对,SpecialTypeHandle是一个特殊的接口,这个方法大概的意思是:先在注入的特殊的类中找是否有参数中传入的type的类型,如果找到了,就去map中找处理这个特殊类的处理器,然后通过处理器来处理值,将处理后的返回值返回。所以SpecialTypeHandle是一个接口,接口定义如下:
public interface SpecialTypeHandle {
public Class getClassSimpleName();
public T getResultObject(Class> eClass,Object value);
}
这样不管是什么样的特殊类型,只要我们定义一个类型的处理器,然后把这个类型加入到特殊类型的MAP中去,就能将正确的值反射进入实体的属性中。贴一个处理Calendar的handle的实现
public class CalendarType implements SpecialTypeHandle {
@Override
public Class getClassSimpleName() {
return Calendar.class;
}
@Override
public Calendar getResultObject(Class> eClass, Object value) {
Calendar calendar = Calendar.getInstance();
if (eClass.getName().equals("java.sql.Timestamp")) {
TimeStamp timeStamp = (TimeStamp) value;
calendar.setTimeInMillis(timeStamp.getTime());
} else if (eClass.getName().equals("java.sql.Date")) {
Date dateValue = (Date) value;
calendar.setTime(dateValue);
}
return calendar;
}
}
至此,我们就完成了大部分封装工作了,是不是很简单,我们还需要进行一些封装,那就是将方法封装出去让用户使用。由于在项目中我们使用了spring,所以整合进spring,这非常的方便,更棒的是有很多需要写代码的工作我们也省却了。比如设置那个特殊类型的map的工作,还有实例化handle的工作,这些都可以让spring来做了。再往上封装就非常的简单了,每个人都有自己的封装的习惯,这里我也就不贴出相关的封装的代码,有需要的可以找我要个。
总结一下:dbutils非常简单,通过简单的改造之后我们也能实现很强大的功能,这是我们想要的结果。我们在进行框架性的东西改造或者封装的时候,我们要对需要处理的框架要透彻的了解,这样可以让我们更加简单,高效,正确的,漂亮的完成工作,当然本屌丝水平有限,班门弄斧的还请见谅!顺便说一句,我们已经将dbutils写成了一个比较通用的orm组件了,测试环境下写测试用例已经很方便了。