引言:最近又用到dbutils,之前一直用Map映射的方式取出select的结果再手工做转换。有写过一篇文章说MapHandler方式的一个缺陷:关于commons dbutils组件的一个小缺陷分析 ,用这种方式,在项目不大的情况下,写一些Map到JavaBean的转换代码工作量不大,但是在数据库表过多并且表中的字段过多的情况下,这种重复的setter感觉有点烦。于是又重新思考了BeanHandler和BeanListHandler的情况,dbutils底层映射用的反射,性能上肯定有损失,不过在大多数项目规模不是很大的情况下,这点损失可以忽略,带来的代码减少却是比较可观。
问题在哪里?先看一段官方的示例代码:
QueryRunner run
=
new
QueryRunner(dataSource);
// Use the BeanHandler implementation to convert the first
// ResultSet row into a Person JavaBean.
ResultSetHandler < Person > h = new BeanHandler < Person > (Person. class );
// Execute the SQL statement with one replacement parameter and
// return the results in a new Person object generated by the BeanHandler.
Person p = run.query(
" SELECT * FROM Person WHERE name=? " , h, " John Doe " );
// Use the BeanHandler implementation to convert the first
// ResultSet row into a Person JavaBean.
ResultSetHandler < Person > h = new BeanHandler < Person > (Person. class );
// Execute the SQL statement with one replacement parameter and
// return the results in a new Person object generated by the BeanHandler.
Person p = run.query(
" SELECT * FROM Person WHERE name=? " , h, " John Doe " );
这里有个地方有约束,就是要求示例中的JavaBean类Person中的字段定义要和数据库的字段定义一致。Java的命名习惯一般是骆峰写法,例如userId,那么数据库中就必须定义为userId,而问题在于:有时候我们需要数据库中字段的定义格式与JavaBean的命名不一样,比如数据库定义为:user_id,而JavaBean则定义为userId
看源代码可能有点费时间,在官方的example页面的最下面果然有一段关于自定义BeanProcessor的指引。摘录出来:
BasicRowProcessor uses a BeanProcessor to convert ResultSet columns into JavaBean properties. You can subclass and override processing steps to handle datatype mapping specific to your application. The provided implementation delegates datatype conversion to the JDBC driver.
BeanProcessor maps columns to bean properties as documented in the BeanProcessor.toBean() javadoc. Column names must match the bean's property names case insensitively. For example, the firstname column would be stored in the bean by calling its setFirstName() method. However, many database column names include characters that either can't be used or are not typically used in Java method names. You can do one of the following to map these columns to bean properties:
1. Alias the column names in the SQL so they match the Java names: select social_sec# as socialSecurityNumber from person
2. Subclass BeanProcessor and override the mapColumnsToProperties() method to strip out the offending characters.
大概意思就是提供二种方式:一种就是最直接的,用as关键字把colName重命名,另一种方式就是继承BeanProcessor类,重写mapColumnsToProperties()方法。
那当然是第二种方式更加具有代表性。尝试了一下。代码如下:
1
public
class
CustomBeanProcessor
extends
BeanProcessor
{
2
3 @Override
4 protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
5 PropertyDescriptor[] props) throws SQLException {
6 int cols = rsmd.getColumnCount();
7 int columnToProperty[] = new int[cols + 1];
8 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
9
10 for (int col = 1; col <= cols; col++) {
11 String columnName = rsmd.getColumnLabel(col);
12 if (null == columnName || 0 == columnName.length()) {
13 columnName = rsmd.getColumnName(col);
14 }
15 columnName = colNameConvent(columnName); // 在这里进行数据库表columnName的特殊处理
16 for (int i = 0; i < props.length; i++) {
17
18 if (columnName.equalsIgnoreCase(props[i].getName())) {
19 columnToProperty[col] = i;
20 break;
21 }
22 }
23 }
24 return columnToProperty;
25 }
26
27 /** *//**
28 * 数据库列名重新约定
29 * @param columnName
30 * @return
31 */
32 private String colNameConvent(String columnName) {
33 String[] strs = columnName.split("_");
34 String conventName = "";
35 for (int i = 0; i < strs.length; i++) {
36 conventName += StringUtils.capitalize(strs[i]);
37 }
38 StringUtils.uncapitalize(conventName);
39 return conventName;
40 }
41}
2
3 @Override
4 protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
5 PropertyDescriptor[] props) throws SQLException {
6 int cols = rsmd.getColumnCount();
7 int columnToProperty[] = new int[cols + 1];
8 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
9
10 for (int col = 1; col <= cols; col++) {
11 String columnName = rsmd.getColumnLabel(col);
12 if (null == columnName || 0 == columnName.length()) {
13 columnName = rsmd.getColumnName(col);
14 }
15 columnName = colNameConvent(columnName); // 在这里进行数据库表columnName的特殊处理
16 for (int i = 0; i < props.length; i++) {
17
18 if (columnName.equalsIgnoreCase(props[i].getName())) {
19 columnToProperty[col] = i;
20 break;
21 }
22 }
23 }
24 return columnToProperty;
25 }
26
27 /** *//**
28 * 数据库列名重新约定
29 * @param columnName
30 * @return
31 */
32 private String colNameConvent(String columnName) {
33 String[] strs = columnName.split("_");
34 String conventName = "";
35 for (int i = 0; i < strs.length; i++) {
36 conventName += StringUtils.capitalize(strs[i]);
37 }
38 StringUtils.uncapitalize(conventName);
39 return conventName;
40 }
41}
注意mapColumnsToProperties方法的逻辑是从父类的方法中直接复制出来的,然后在第15行那里变了个戏法,这里的columnName就是从数据库中读出来的,自定义一个private方法用于转换命名,这里你就可以添加自己的命名约束。例如上面就是把 user_id 转化为Java的骆峰写法:userId
再深入一层思考,你可以在这里进行更多扩展,以便让自己可以选择不同的命名转换方式。定义了这个Processor之后,下面看看如何调用:
Connection conn
=
ConnectionManager.getInstance().getConnection();
QueryRunner qr = new QueryRunner();
CustomBeanProcessor convert = new CustomBeanProcessor();
RowProcessor rp = new BasicRowProcessor(convert);
BeanHandler < User > bh = new BeanHandler < User > (User. class , rp);
User u = qr.query(conn, sql, bh, params);
DbUtils.close(conn);
是不是非常灵活?如果是想返回List结果的,就把BeanHandler替换成BeanListHander类,还可以再进一步封装这些操作,抽象到公共模块中去,让外部直接传入sql语句和Class就能直接返回想要的结果,当然你得增加泛型的定义。同样举一个封装的例子:
QueryRunner qr = new QueryRunner();
CustomBeanProcessor convert = new CustomBeanProcessor();
RowProcessor rp = new BasicRowProcessor(convert);
BeanHandler < User > bh = new BeanHandler < User > (User. class , rp);
User u = qr.query(conn, sql, bh, params);
DbUtils.close(conn);
1
protected
<
T
>
List
<
T
>
selectBeanList(Connection conn, String sql, Class
<
T
>
type,
2 Object[] params) throws Exception {
3 log.debug("select sql:[" + sql + "]");
4 QueryRunner qr = new QueryRunner();
5 CustomBeanProcessor convert = new CustomBeanProcessor();
6 RowProcessor rp = new BasicRowProcessor(convert);
7 ResultSetHandler<List<T>> bh = new BeanListHandler<T>(type, rp);
8 List<T> list = qr.query(conn, sql, bh, params);
9 return list;
10 }
2 Object[] params) throws Exception {
3 log.debug("select sql:[" + sql + "]");
4 QueryRunner qr = new QueryRunner();
5 CustomBeanProcessor convert = new CustomBeanProcessor();
6 RowProcessor rp = new BasicRowProcessor(convert);
7 ResultSetHandler<List<T>> bh = new BeanListHandler<T>(type, rp);
8 List<T> list = qr.query(conn, sql, bh, params);
9 return list;
10 }
至于为什么扩展这个方法就可以实现这个逻辑就得去跟源代码看它的内部实现,用了一些JavaBean的处理和反映的技巧来做的。具体就不说。
总结:commons组件都设计得非常好,可扩展性和实用性都非常高。虽然上面举例实现了转换逻辑的替换,但是仍然需要开发人员在设计数据库的时候和写JavaBean时都要严格做好规范,避免产生不必要的问题。这方面Ruby On Rails就直接内部实现,动态语言的优点特别能体现,同时强制你在设计时必须用这种方式,典型的约定优于配置原则。当然,在dbutils里你愿意二种字段名都一样也无可厚非。
缺点:BeanProcessor是不支持关联查询的,所以上面的方式也只能局限于单表的转换,这点就不如myBatis和Hibernate,当然用这二个就引入了一些复杂性,如何权衡需要自己衡量,哪个用得好都一样。本人就不喜欢myBatis那种把SQL写到XML中的方式,见过太复杂的SQL最终在XML里面变得面目全非,如果是接手别人的代码,是很痛苦的,而且你无法避免只修改XML而不改Java,既然二者都要改,那直接写Java里又有什么区别?简单就是美。格式和注释写好一点同样很容易理解!
刚进场的时候戏就落幕