当前位置: 首页 > article >正文

简明 JDBC 数据访问操作库:JdbcHelper(一)

开篇

综观后台开发,大多数的开发工作就是数据库的开发。怎么跟数据库打好交道与我们的开发体验息息相关。业界典型的讨论“对象关系阻抗不匹配”正是问题的症结所在:既然都是两种不同的“编程世界观”——一个数据库、一个编程语言,各自有各自的方法论、甚至思维都不太一样,怎么可以做到“和衷共济”呢?所以问题的关键就是如何解决这个“阻抗不匹配”难题——数据库模型和编程语言模型之间的差异所导致的“难题”。凭借多年的开发经验,笔者斗胆给出一个清晰、简单、可复用的 JDBC 解决方案,务求尝试可以处理数据库也就是 Java 领域中 JDBC 所围绕的“CRUD(增删改查)”问题。当然,“肺话(废话)”还是得先说一说,这只代表笔者本人的经验和水平,可能有考虑不周的情况,但容笔者再三审视,此乃算是比较通俗化、适谱性较强的方案,——当然咱不走偏锋,搞太多稀奇古怪的轮子。其中有些大家争论的地方,笔者看如今可以有个结论了:

  • 承认 SQL 首要(SQL First)。很多后台开发者为了避开写 SQL,于是在各种 ORM 方案绞尽脑汁,笔者看大可不必。ORM 看似开发效率奇高,但实际“里三层、外三层”——不过封装罢了,终究还是回到 SQL 层,也就是说,写 ORM 搞不定,你还是要写 SQL
  • 复杂逻辑的 SQL 不允许你写 ORM,或者说写 SQL 更舒服简单,为什么要弄蹩脚的 ORM 呢?实际 SQL 本身足够优秀,不就写多种语言嘛。为什么开发者只能用一种语言呢?你看前端的,虽然主打 js/ts,但人家 HTML/CSS 也不拉下呀——可见多语言开发是常态,不必排斥学习
  • 业界也给出最终的答案,当时 iBatis/MyBatis 代替了 Hibernate,即是一例

综上述,就是要 SQL 与编程语言分离。如何分离呢?怎么界定好两者之间的接口正是问题关键所在。在 Java 中,大家尝试用 DAO(Data Access Object)层去解决。我们这里讨论的,就是这个 DAO。

前面说 MyBatis 的框架是最终答案,为什么不直接用就好呢?MyBatis 笔者也比较喜欢,也不是说反对使用 MyBatis,而是笔者希望有自己更简单的 DAO。没有接触 MyBatis 之前,笔者已经有自己几次尝试写 ORM 的累积,虽然不太完美,但也不想放弃,于是再继续复盘、重构、优化。因此在前几次的基础,再推出一次新的 JDBC DAO,——希望也是最后一次的方案。

源码揽胜

话不多说,让我们直接进入源码看看。源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-framework/aj-framework/src/main/java/com/ajaxjs/data。

连接数据库

首先是处理数据库连接的,JdbcConn,之前的版本都是静态方法,在 static 方法中传入 Connection 对象,Connection 太常见了,几乎每次都需要传入。这次重构改掉之前喜欢过程式的风格,采用 Java OO 方式(好像这不是什么问题,但对于“老人”来说习惯使然就是毛病)。

创建数据库连接

connection 作为 JdbcConn 的一个 field 出现。这意味着,通过 getter/setter 读写此 connection,或者使用 Spring 的注入。

/**
 * 数据库连接
 */
public class JdbcConn {
    private static final LogHelper LOGGER = LogHelper.getLog(JdbcConn.class);

    /**
     * 数据库连接对象
     */
    Connection connection;
    ……
}

JDBC 最质朴的连接数据库方法如下。有次试过不能把 user 和 password 写在第一个 jdbc 连接字符串上,那样会连不通,死活不行(估计要转义某个字符串),分开 user 和 password 就可以。

/**
 * 连接数据库。这种方式最简单,但是没有经过数据库连接池。
 * 有时不能把 user 和 password 写在第一个 jdbc 连接字符串上 那样会连不通
 * 分开 user 和 password 就可以
 *
 * @param jdbcUrl  数据库连接字符串,不包含用户名和密码
 * @param userName 用户
 * @param password 密码
 * @return 数据库连接对象
 */
public Connection getConnection(String jdbcUrl, String userName, String password) {
    try {

        if (StringUtils.hasText(userName) && StringUtils.hasText(password))
            connection = DriverManager.getConnection(jdbcUrl, userName, password);
        else connection = DriverManager.getConnection(jdbcUrl);

        LOGGER.info("数据库连接成功: " + connection.getMetaData().getURL());
    } catch (SQLException e) {
        LOGGER.warning("数据库连接失败!", e);
    }

    return connection;
}

重载一个版本,数据库连接字符串,已包含用户名和密码的。


 /**
  * 连接数据库。这种方式最简单,但是没有经过数据库连接池。
  *
  * @param jdbcUrl 数据库连接字符串,已包含用户名和密码
  * @return 数据库连接对象
  */
 public Connection getConnection(String jdbcUrl) {
     return getConnection(jdbcUrl, null, null);
 }

这个最质朴的方法,注意是没有经过数据库连接池的,一般测试用或者简单场合用。

至于使用连接池的,笔者这里给出一个使用 Tomcat JDBC Pool 的,如 setupJdbcPool()

/**
 * 手动创建连接池。这里使用了 Tomcat JDBC Pool
 *
 * @param driver   驱动程序,如 com.mysql.cj.jdbc.Driver
 * @param url      数据库连接字符串
 * @param userName 用户
 * @param password 密码
 * @return 数据源
 */
public static DataSource setupJdbcPool(String driver, String url, String userName, String password) {
    PoolProperties p = new PoolProperties();
    p.setDriverClassName(driver);
    p.setUrl(url);
    p.setUsername(userName);
    p.setPassword(password);
    p.setMaxActive(100);
    p.setInitialSize(10);
    p.setMaxWait(10000);
    p.setMaxIdle(30);
    p.setMinIdle(5);
    p.setTestOnBorrow(true);
    p.setTestWhileIdle(true);
    p.setTestOnReturn(true);
    p.setValidationInterval(18800);
    p.setDefaultAutoCommit(true);
    org.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();
    ds.setPoolProperties(p);

    return ds;
}

大家都喜欢用 Druid、HikariCP,——但笔者比较喜欢轻量级的 Tomcat 的,相当于集成自带。

连接字符串说不定什么时候会用到,也记一个吧:

/**
 * 一般情况用的数据库连接字符串
 */
public static final String JDBC_TPL = "jdbc:mysql://%s/%s?characterEncoding=utf-8&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai";

还有个不得不提的是从 DataSource 获取 Connection 。DataSource 其实比 Connection 更重要,特点有三:

  • 可以从 DataSource 获取 Connection(如下方法),是其根源;
  • DataSource 一般代表已经池化了的;
  • 于是这个 DataSource 才是 API 交换的接口(实际也是 Java Interface)。君不见如 Spring JDBC 也是要求你传入 DataSource 而不是 Connection 的,所以框架级的暴露一个数据库对象应该是 DataSource 而不是Connection。
/**
 * 根据数据源对象获得数据库连接对象
 *
 * @param source 数据源对象
 * @return 数据库连接对象
 */
public Connection getConnection(DataSource source) {
    try {
        connection = source.getConnection();

        if (connection == null) LOGGER.warning("DataSource 不能建立数据库连接");
    } catch (SQLException e) {
        LOGGER.warning(e, "通过数据源对象获得数据库连接对象失败!");
    }

    return connection;
}

当前进程的数据库连接

这个就是日常使用的 Connection,每次控制器先会从这里获取数据库连接。大概就是通过 ThreadLocal 保存、获取,非常方便。

既然是 ThreadLocal 的——那就是 static 静态方法了。

/**
 * 当前进程的数据库连接
 */
private static final ThreadLocal<Connection> CONNECTION = new ThreadLocal<>();

/**
 * 获取一个当前进程的数据库连接
 *
 * @return 当前进程的数据库连接对象
 */
public static Connection getConnection() {
    return CONNECTION.get();
}

/**
 * 保存一个数据库连接对象到当前进程
 *
 * @param conn 当前进程的数据库连接对象
 */
public static void setConnection(Connection conn) {
    CONNECTION.set(conn);
}

/**
 * 关闭当前进程的数据库连接
 */
public static void closeDb() {
    closeDb(getConnection());
    CONNECTION.set(null);
}

/**
 * 关闭数据库连接
 *
 * @param conn 数据库连接对象
 */
public static void closeDb(Connection conn) {
    try {
        if (conn != null && !conn.isClosed()) {
            conn.close();
            if (Version.isDebug)
                LOGGER.info("关闭数据库连接成功! Closed database OK!");
        }
    } catch (SQLException e) {
        LOGGER.warning(e);
    }
}

顺带提供关闭数据库连接的静态方法。一般在控制器执行完业务方法之后统一关闭。

JdbcReader vs. JdbcWriter

标题党,其实没什么好比较的,就是一个读、一个写,安排在两个类而已。它们都继承 JdbcConn。

JdbcReader

如下,executeQuery() 属于底层的方法,调用 PreparedStatement 进行查询。开始时候还想要封装原始的 Statement 对象,如今看没什么意义,一概使用 PreparedStatement 即可。

先明确一个事情,但凡与 DBMS 沟通,不消多说就是传入字符串的 SQL;在使用 PreparedStatement 的情况下,SQL 可以是带 占位符的 ? 的,于是有了另外一个参数:参数列表,可以是 Object[],传给 PreparedStatement 就行。下面我们无论 CRUD,都是这样的结构:SQL:Stringparams: Object[](可选,不传也行,表示只执行 SQL 即可)

/**
 * 执行查询
 *
 * @param <T>       结果的类型
 * @param processor 结果处理器
 * @param sql       SQL 语句
 * @param params    插入到 SQL 中的参数,可单个可多个可不填
 * @return 查询结果,如果为 null 表示没有数据
 */
public <T> T executeQuery(ResultSetProcessor<T> processor, String sql, Object... params) {
    try (PreparedStatement ps = connection.prepareStatement(sql)) {
        setParam2Ps(ps, params);

        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next())
                return processor.process(rs);
            else {
                LOGGER.info("查询 SQL:{0} 没有符合的记录!", sql);
                return null;
            }
        }
    } catch (SQLException e) {
        LOGGER.warning(e);
    }

    return null;
}

查询是否有数据这里是 rs.next(),但好像不同数据库有不同的方法,SQLite 记得好像就不是了,先不管了。只考虑 MySQL 的情况。

ResultSetProcessor<T> processor 是一个自定义 lambda,用于如何转换 ResultSet 到目标结果的处理器。

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 如何转换 ResultSet 到目标结果的处理器
 *
 * @param <T> 结果的类型
 */
@FunctionalInterface
public interface ResultSetProcessor<T> {
    /**
     * 转换结果
     *
     * @param rs JDBC 结果集合
     * @throws SQLException SQL 异常
     */
    T process(ResultSet rs) throws SQLException;
}

当然你可以用 JDK 原生的 Function 类型的,无须自定义 lambda。之所以自定义 lambda,为了是可以确定参数 ResultSet rs

JDBC 的 ResultSet 不能直接使用,一般都是转换到目标类型,如 Map 或者 Java Bean,

/**
 * 记录集合转换为 Map
 *
 * @param rs 记录集合
 * @return Map 结果
 * @throws SQLException 转换时的 SQL 异常
 */
public static Map<String, Object> getResultMap(ResultSet rs) throws SQLException {
    // LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序
    Map<String, Object> map = new LinkedHashMap<>();
    ResultSetMetaData metaData = rs.getMetaData();

    for (int i = 1; i <= metaData.getColumnCount(); i++) {// 遍历结果集
        String key = JdbcUtil.changeColumnToFieldName(metaData.getColumnLabel(i));
        Object value = rs.getObject(i);

        map.put(key, value);
    }

    return map;
}

看上去这是普通的 Java static 方法,但也符合 ResultSetProcessor<T> 之定义,可作 lambda 用,于是 JdbcReader::getResultMap 这样是 OK 的,下面就是使用例子。

 /**
  * 查询单行记录(单个结果),保存为 Map&lt;String, Object&gt; 结构。如果查询不到任何数据返回 null。
  *
  * @param sql    SQL 语句,可以带有 ? 的占位符
  * @param params 插入到 SQL 中的参数,可单个可多个可不填
  * @return Map&lt;String, Object&gt; 结构的结果。如果查询不到任何数据返回 null。
  */
 public Map<String, Object> queryAsMap(String sql, Object... params) {
     return executeQuery(JdbcReader::getResultMap, sql, params);
 }

这方法只考虑单行记录查询的,至于列表则参考 queryAsMapList(),篇幅关系不多说了。


http://www.kler.cn/news/16877.html

相关文章:

  • Redis的哨兵和集群模式
  • Figma快速转换为Sketch文件格式的方法
  • 【软考高级】2017年系统分析师论文真题
  • ChatGPT根据销售数据、客户反馈、财务报告,自动生成报告,并根据不同利益方的需要和偏好进行调整?
  • Spring 5 笔记 - 入门与IOC
  • 【华为OD机试 2023最新 】最大报酬(C语言题解 100%)
  • 大数据技术之Hadoop-入门
  • shell脚本的循环
  • 『python爬虫』异常错误:request状态码是200,但是使用full xpath路径解析返回得到是空列表(保姆级图文)
  • Vue.js核心概念简介:组件、数据绑定、指令和事件处理
  • 1985-2021年全国31省一二三产业就业人数/各省分产业就业人数数据(无缺失)
  • 【C++】-关于类和对象的默认成员函数(中)-构造函数和析构函数
  • 操作系统——操作系统用户界面
  • C++入门(下)
  • 给你们讲个笑话——低代码会取代程序员
  • 360SEO 360搜索引擎算法的基础知识
  • Shell脚本3
  • 代码优美,搬砖不累:探索高质量代码之路
  • [架构之路-188]-《软考-系统分析师》-3-操作系统 - 图解页面替换算法LRU、LFU
  • 操作系统——第三章
  • 【FATE联邦学习】FATE是否支持batch分批训练?
  • 现代CMake高级教程 - 第 1 章:添加源文件
  • PowerJob基本概念
  • PHP学习笔记第一天
  • PHP+vue大学生心理健康评价和分析系统8w3ff
  • 每天一点C++——杂记
  • QT文本编辑与排版包含字体相关设置、段落对齐与排序方式
  • 树的刷题,嗝
  • 如果用上以下几种.NET EF Core性能调优,那么查询的性能会飙升
  • bash的进程与欢迎讯息自定义