综观后台开发,大多数的开发工作就是数据库的开发(俗称“CRUD Boy”)。怎么跟数据库打好交道与我们的开发体验息息相关。业界时常讨论的“对象关系阻抗不匹配(Object–relational impedance mismatch)”正是问题的症结所在:既然都是两种不同的“编程世界观”——一个关系型模型的数据库(SQL)、一个编程语言(Java、C#、Go), 各自有各自的方法论、使用场景及思维都不太一样,那么怎么可以做到“和衷共济”然后“互通有无”呢?显然而言“阻抗不匹配”不是一个容易解决的问题。很多后台开发者为了避开写 SQL,于是在各种 ORM(Object–relational mapping) 方案中绞尽脑汁,——请恕笔者直言——那大可不必。ORM 看似开发效率奇高,但实际“里三层、外三层”——不过封装罢了,终究还是回到 SQL 层。ORM,通俗讲就是把一种问题转化为另一种问题进行解决,完全抛弃 SQL。但是数据库的问题,比如关联查询、嵌套的子查询和复杂逻辑的 SQL 等,能在 ORM 中完美地解决吗?或者说写起来并不那么舒服简单,与其这样为什么还要弄蹩脚的 ORM?ORM 恐怕心有余而力不足。而那些问题却是关系数据库最擅长的问题域。把关系数据库擅长解决的问题转化给不擅长处理这类问题的 ORM 去解决,岂不是挺糊涂的吗?面向对象的方法论(ORM 亦算 OO 之衍生),应当控制一下自己的野心,专注于自己擅长的领域吧。况且 SQL 本身足够优秀优雅,都是值得我们深入研究的。
当时 iBatis/MyBatis 代替了 Hibernate,即是一例,证明还不如写 SQL 灵活、高效(SQL First/SQL Centric)。后来的 MyBatis Plus 的 lambda 写法却有点走回头路的意思,——实际早年微软的 LinQ 已大行其道。
当然,也不是说完全抛起 ORM 中的优秀部分,例如:
参阅: ↗ 《ORM(对象关系映射)的灾难》。
结合上述的优缺点,笔者斗胆给出一个清晰、简单、可复用的 JDBC 解决方案,务求尝试可以处理数据库也就是 Java 领域中 JDBC 所围绕的“CRUD(增删改查)”问题。依笔者浅见,怎么界定好两者之间的接口正是问题关键所在。在 Java 中大家尝试用 DAO(Data Access Object)层去解决,我们这里讨论的就是这个 DAO。
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);
大家都喜欢用 Druid、HikariCP,——但笔者比较喜欢轻量级的 Tomcat 的,相当于集成自带。
还有个不得不提的是从 DataSource 获取 Connection 。DataSource 其实比 Connection 更重要,特点有三:
/** * 根据数据源对象获得数据库连接对象 * * @param source 数据源对象 * @return 数据库连接对象 */ public Connection getConnection(DataSource source);
这个就是日常使用的 Connection,每次控制器先会从这里获取数据库连接。大概就是通过 ThreadLocal 保存、获取,非常方便。既然是 ThreadLocal 的——那就是 static 静态方法了。
/** * 当前进程的数据库连接 */ private static final ThreadLocalCONNECTION = 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); } }
顺带提供关闭数据库连接的静态方法。一般在控制器执行完业务方法之后统一关闭。