从 apache duutils 所学到的

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

这几天在研究 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)给自己带来了一个问题,如果使用抽象的方式,来实现分页.

 

最后,感谢你的阅读和浏览,希望我们共同进步.


本文转自peiquan 51CTO博客,原文链接:http://blog.51cto.com/peiquan/1420159


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
存储 缓存 Apache
小白带你学习linuxAPACHE安装和管理 (二十)
小白带你学习linuxAPACHE安装和管理 (二十)
38 0
|
10月前
|
安全 API Apache
Apache服务深入学习篇(详细介绍)
Apache服务深入学习篇(详细介绍)
408 0
Apache服务深入学习篇(详细介绍)
|
SQL 机器学习/深度学习 分布式计算
适合小白入门Spark的全面教程(二)
适合小白入门Spark的全面教程(二)
578 0
适合小白入门Spark的全面教程(二)
|
SQL 分布式计算 Hadoop
适合小白入门Spark的全面教程(一)
适合小白入门Spark的全面教程(一)
235 0
适合小白入门Spark的全面教程(一)
|
存储 分布式计算 资源调度
【Spark】【复习】Spark入门考前概念相关题复习
【Spark】【复习】Spark入门考前概念相关题复习
289 0
|
关系型数据库 MySQL 应用服务中间件
Apache的安装与使用经验| 学习笔记
快速学习Apache的安装与使用经验。
82 0
|
关系型数据库 MySQL 应用服务中间件
Apache的安装与使用经验
一、课程安排 二、LAMP架构应用经验 三、Apache使用经验
|
SQL 存储 分布式计算
「Spark从精通到重新入门(二)」Spark中不可不知的动态资源分配
资源是影响 Spark 应用执行效率的一个重要因素。Spark 应用中真正执行 task 的组件是 Executor,可以通过spark.executor.instances 指定 Spark 应用的 Executor 的数量。在运行过程中,无论 Executor上是否有 task 在执行,都会被一直占有直到此 Spark 应用结束。
558 0
「Spark从精通到重新入门(二)」Spark中不可不知的动态资源分配