Posted on 2011-04-26 16:41
寒武紀 閱讀(4266)
評論(0) 編輯 收藏 所屬分類:
數據庫 、
Java
引言:最近又用到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");
這里有個地方有約束,就是要求示例中的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
}
注意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就能直接返回想要的結果,當然你得增加泛型的定義。同樣舉一個封裝的例子:
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
}
至于為什么擴展這個方法就可以實現這個邏輯就得去跟源代碼看它的內部實現,用了一些JavaBean的處理和反映的技巧來做的。具體就不說。
總結:commons組件都設計得非常好,可擴展性和實用性都非常高。雖然上面舉例實現了轉換邏輯的替換,但是仍然需要開發人員在設計數據庫的時候和寫JavaBean時都要嚴格做好規范,避免產生不必要的問題。這方面Ruby On Rails就直接內部實現,動態語言的優點特別能體現,同時強制你在設計時必須用這種方式,典型的約定優于配置原則。當然,在dbutils里你愿意二種字段名都一樣也無可厚非。
缺點:BeanProcessor是不支持關聯查詢的,所以上面的方式也只能局限于單表的轉換,這點就不如myBatis和Hibernate,當然用這二個就引入了一些復雜性,如何權衡需要自己衡量,哪個用得好都一樣。本人就不喜歡myBatis那種把SQL寫到XML中的方式,見過太復雜的SQL最終在XML里面變得面目全非,如果是接手別人的代碼,是很痛苦的,而且你無法避免只修改XML而不改Java,既然二者都要改,那直接寫Java里又有什么區別?簡單就是美。格式和注釋寫好一點同樣很容易理解!
剛進場的時候戲就落幕