從 apache duutils 所學到的

這幾天在研究 dbutils 的源碼,感覺收穫很大,至於說到收穫了什麼,我也很難描述的清楚,便想借助下面的圖和文字來描述自己的所學和所得。

在開始本文之前,我給自己帶來了一些疑問,就是通過原始的sql 實現無浸入式的分頁工具,一直很想仿 dbutil 的思想,可是還是實現不了。希望有高手可以一些指點。

dbutil 是對 jdbc 進行了一些簡單的封裝,有人說 dbutil 的強大之處在於其強大的結果集處理,這個不可否認。不過通過這幾天在臨摹 dbutil 的實現時,我覺得更爲強大的是其背後的設計邏輯(而本人也是通過這種邏輯來實現一個分頁工具,不過暫未實現)。先看一個基礎圖(建議下載下來看):

dbutil

1、首先,我們從一個簡單查詢開始談起,在 QueryRunner 裏有這麼一個查詢方法,如下:

public <T> T query(String sql, ResultSetHandler<T> rsh, Object... params);

簡單的介紹一下:這個方法其實很容易理解,sql 即 select 的查詢語句,params 爲 sql 的參數,而 rsh 就是一個結果集處理器,通過這個結果集處理器,可以返回我們所需要的類型,如Map<String,Object>,Object[],List<Map<String,Object>> 和 javabean 等。所以我認同 dbutil 的一點就是:

dbutil 可靈活定製的返回的類型。

2、ResultSetHandler<T> 是一個接口,它只有一個方法 void handle(ResultSet rs)。如我們想返回一個 Map<String,Object> 對象,可以這樣做:

ResultSetHandler<Map<String,Object>> handler = new ResultSetHandler<Map<String,Object>> (){
    public Object[] handle(ResultSet rs) throws SQLException {
        if (!rs.next()) {
            return null;
        }
    
        ResultSetMetaData meta = rs.getMetaData();
        int cols = meta.getColumnCount();
        Map<String,object> row = new HashMap<String,Object>();
 
        for (int i = 1; i <= cols; i++) {
            row.put(meta .getColumnName(i), rs.getObject(i));
        }
 
        return row;
    }
};

執行如下: query(sql,handler,params) 即可。

不過作爲一個包,肯定會爲我們提供一些基礎的實現,而其強大的設計思想,正是讓我佩服的地方,看一下圖:

image

在一個 resultSet 裏,可能包含裏包含了多行數據,所以在這裏抽象出來一個 RowProcessor 行處理器,而我們就需要將每一行數據轉化我們需要的類型,進而方便我們返回需要的List<T> 類型。接着解釋一下上面這幅圖的其他含義:

BasicRowProcessor ,一個基礎的行處理器,也是 dbutils 裏的默認的行處理器,其提供了 tiArray(),toBean(),toMap() 的基礎實現(代碼可以參考 dbutils 的源碼或下載附件參考其代碼的實現)。

@Override
public Object[] toArray(ResultSet rs) throws SQLException {
    ResultSetMetaData metaData = rs.getMetaData();
    int counts = metaData.getColumnCount();
    Object[] result = new Object[counts];
    
    for(int i = 1;i<counts;i++){
        result[i] = rs.getObject(i);
    }
    
    return result;
}
 
@Override
public Map<String, Object> toMap(ResultSet rs) throws SQLException {
    Map<String,Object> result = new HashMap<String, Object>();
    ResultSetMetaData metaData = rs.getMetaData();
    int counts = metaData.getColumnCount();
    if (counts == 0) {
        return null;
    }
    for(int i =1;i < counts;i++){
        result.put(metaData.getColumnName(i), rs.getObject(i));
    }
    return result;
}

BeanProcessor 是一個將一行數據轉化爲一個指定的 javabean,在這裏需要藉助一些java反射的技巧,不懂的可以參考 這篇文章,在這裏還需要將 數據庫裏的 null 轉化爲對應javabean 屬性的默認值等,在此不作探討。

MapHandler 是將一行數據轉化爲一個 Map<String,Object> , 使用了默認行處理器 BasicRowProcessor ,如遇到特殊情況,也可以自定義一個MapHandler 的行處理方式 convert。MapHandler 的構造函數如下:

public class MapHandler implements ResultSetHandler<Map<String, Object>> {
 
    private final RowProcessor convert;
    
    public MapHandler(){
        this(new BasicRowProcessor());    // 使用默認的行處理器
    }
    
    public MapHandler(RowProcessor convert){
        super();
        this.convert = convert;
    }
    
    @Override
    public Map<String, Object> handle(ResultSet rs) throws SQLException {
        return rs.next()?this.convert.toMap(rs):null;
    }
}

3、理解一下 List<T> 的處理方式,這裏很好地使用抽象類,如圖:

image

首先,AbstractListHandler 對 RowProcessor 進行一層很好的 List<T> 抽象,如下:

public abstract class AbstractListHandler<T> implements ResultSetHandler<List<T>> {
 
    @Override
    public List<T> handle(ResultSet rs) throws SQLException {
        List<T> rows = new ArrayList<T>();
        while (rs.next()) {
            rows.add(this.handleRow(rs));
        }
         return rows;
    }
    
    /**
     * convert     row into some java objects
     * @param rs
     * @return
     */
    protected abstract T handleRow(ResultSet rs) throws SQLException;
}

AbstractListHandler 抽象每一個 ListHandler 都需要的 – 往 list 添加一個元素(如代碼裏handle(ResultSet rs)),這樣每一個 ListHandler 就只需要關注自己如何去處理一行數據就行了,說到這裏我們就會想到可以使用默認的行處理器 BasicRowProcessor,(如有需要可定製),我們參考一下,MapListHandler  的實現(其實現超級簡單,靈活的應用了我們一開始定義的行處理器)。

public class MapListHandler extends AbstractListHandler<Map<String,Object>> {
 
    private final RowProcessor convert;
    
    public MapListHandler() {
        this(ArrayHandler.ROW_PROCESSOR);
    }
    
    public MapListHandler(BasicRowProcessor convert) {
        this.convert = convert;
    }
 
    @Override
    protected Map<String,Object> handleRow(ResultSet rs) throws SQLException {
        return this.convert.toMap(rs);
    }
 
}

4、總結,在最後,我依然是無法準確去表述自己學到了什麼,不過我知道了dbutil 進行了一層很好的抽象,靈活地重用代碼。對已結果集處理器,很強大,而我的理解是,在函數裏使用接口來描述處理方式,可以帶來靈活的拓展,至於更深的,還在努力探討中,也希望優秀的你可以給我一些指導和提示,謝謝。

5.拓展:在學 dbutil 時,爲了驗證自己的學習成果,我從其中抽出了自己的常用的功能代碼,並做了一些自己的封裝,在此自己羅列總結一下:

1)從 properties 加載連接數據庫的參數:

private static String driver_class_name = null;
private static String url = null;
private static String username = null;
private static String password = null;
 
static {
    // 使用properties 文件加載屬性文件
    Properties properties = new Properties();
    try {
        properties.load(new FileInputStream("src\\jdbc.properties")); // 脫離ide,會報錯
//            properties.load(Files.newInputStream(Paths.get("src\\jdbc.properties")));
    } catch (IOException e) {
        e.printStackTrace();
    }
    driver_class_name = properties.getProperty("driverClassName");
    url = properties.getProperty("url");
    username = properties.getProperty("username");
    password = properties.getProperty("password");
}

2)在插入一行數據時,使用兩種方式返回自增的主鍵:

// 插入一條語句並返回其主鍵
 public Object insertAndReturnKey(Connection conn, boolean isClosedConnection, String sql)
        throws SQLException {
    if (conn == null) {
        throw new SQLException("null connection");
    }
    if (sql == null) {
        close(conn);
        throw new SQLException("null sql statement");
    }
 
    Statement stmt = null;
    ResultSet rs = null;
    try {
        stmt = conn.createStatement();
        stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
        rs = stmt.getGeneratedKeys();
        while (rs.next()) {
            return rs.getObject(1);
        }
        return null;
    } finally {
        close(rs);
        close(stmt);
        if (isClosedConnection) {
            close(conn);
        }
    }
}
 
// 使用mysql 的函數 last_insert_id() 返回自增主鍵
 public Object insertAndReturnKeyMySQL(Connection conn, boolean isClosedConnection, String sql,
        Object... params) throws SQLException {
    if (conn == null) {
        throw new SQLException("null connection");
    }
    if (sql == null) {
        close(conn);
        throw new SQLException("null sql statement");
    }
 
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
        conn.setAutoCommit(false);
        stmt = this.preparedStatement(conn, sql);
        this.fillStatement(stmt, params);
        stmt.executeUpdate();
        // mysql 的函數
        rs = stmt.executeQuery("select LAST_INSERT_ID();");
        conn.commit();
        if (rs.next()) {
            return rs.getObject(1);
        } else {
            conn.rollback();
            return null;
        }
    } finally {
        close(rs);
        close(stmt);
        if (isClosedConnection) {
            close(conn);
        }
    }
 
}

不好的地方是,使用 原始java 代碼時,只接受完整的sql語句,不能使用 PrepareStatement。使用 mysql 的函數,就與mysql耦合了。

3)模擬 jdbc裏部分實現:

// 模擬 spring jdbc queryForMap,不過有一點不同的是,當沒有查詢結果時,會返回null,而不會拋出異常.
public Map<String, Object> queryForMap(Connection conn, String sql, Object... params) {
    try {
        return query(conn, true, sql, new MapHandler(), params);
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return null;
}
 
// 模擬 spring jdbc 的 queryForMap
public List<Map<String, Object>> queryForListMap(Connection conn, String sql, Object... params) {
    try {
        return query(conn, true, sql, new MapListHandler(), params);
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return new ArrayList<Map<String,Object>>();
}

4)給自己帶來了一個問題,如果使用抽象的方式,來實現分頁.

 

最後,感謝你的閱讀和瀏覽,希望我們共同進步.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章