《手写Spring渐进式源码实践》实践笔记(第十八章 JDBC功能整合)
第十八章 JDBC功能整合
背景
技术背景
JDBC
JDBC(Java Database Connectivity)是一个Java API,用于连接和执行查询在数据库中。它提供了一种标准的方法,允许Java程序
连接到各种关系型数据库
,执行SQL语句,并处理结果
。JDBC是一个强大的工具,它为Java应用程序提供了与数据库交互的能力,无论是进行数据检索还是更新。通过使用JDBC,开发者可以编写可移植的代码,这些代码可以在不同的数据库系统之间运行,而无需对代码进行大量修改。
以下是JDBC的一些关键点介绍:
- 数据库连接:
- JDBC使用
DriverManager
类来管理数据库连接。通过传递数据库URL、用户名和密码,可以获取到一个Connection
对象,该对象代表与特定数据库的连接。
- JDBC使用
- 驱动程序:
- JDBC驱动程序是一个允许Java应用程序与数据库进行交互的软件组件。JDBC驱动程序可以是类型1(原生方法)、类型2(基于Java的独立驱动程序)、类型3(纯Java驱动程序,使用JDBC网络协议)、类型4(JDBC驱动程序,使用数据库的薄客户端)。
- Statement和PreparedStatement:
Statement
对象用于执行静态SQL语句,并返回它所生成的结果。PreparedStatement
是Statement
的子接口,它允许预编译SQL语句,可以提高性能并防止SQL注入攻击。
- 执行SQL语句:
- 通过
Statement
或PreparedStatement
对象,可以执行各种SQL语句,包括查询(SELECT)、更新(UPDATE、DELETE)、插入(INSERT)和DDL(CREATE、DROP等)语句。
- 通过
- 处理结果:
- 查询数据库后,可以通过
ResultSet
对象处理返回的数据。ResultSet
提供了一种方式来遍历查询结果集中的数据。
- 查询数据库后,可以通过
- 事务处理:
- JDBC支持事务处理,默认情况下,每个
Statement
执行的SQL语句都是自动提交的。可以通过Connection
对象来设置自动提交模式,并使用commit()
和rollback()
方法来管理事务。
- JDBC支持事务处理,默认情况下,每个
- 批处理:
- JDBC提供了批处理功能,允许一次执行多个SQL语句,这可以减少网络往返次数,提高性能。
- 元数据:
DatabaseMetaData
接口提供了关于数据库的整体元数据信息,如表的结构、存储过程、支持的SQL语法等。
- 关闭资源:
- 为了释放数据库资源,应该在使用完毕后关闭
ResultSet
、Statement
和Connection
对象。
- 为了释放数据库资源,应该在使用完毕后关闭
- 异常处理:
- JDBC操作可能会抛出
SQLException
,因此需要适当的异常处理机制来确保程序的健壮性。
- JDBC操作可能会抛出
JdbcTemplate
JdbcTemplate
是 Spring 框架中提供的一个用于简化 JDBC 编程的模板类。它处理了资源的创建和释放,并且提供了执行 SQL 语句和查询的便捷方法,从而减少了编写 JDBC 代码时常见的样板代码。
以下是 JdbcTemplate
的一些关键特性和用法:
关键特性
-
简化资源管理:
JdbcTemplate
管理数据库连接,确保在每次执行后都正确关闭连接,从而避免了资源泄漏。 -
异常处理:它将 JDBC 的
SQLException
转换为 Spring 的DataAccessException
,这使得异常处理更加一致和易于管理。 -
便捷的方法:提供了多种便捷方法来执行 SQL 语句,包括
update
(用于执行插入、更新、删除等)、query
(用于执行查询)以及queryForObject
、queryForMap
、queryForList
等。 -
参数化查询:支持使用
PreparedStatement
来执行参数化查询,从而防止 SQL 注入攻击。 -
结果集处理:提供了将结果集映射到 Java 对象的功能,包括将单行结果映射到一个对象,将多行结果映射到一个列表等。
用法示例
以下是一个简单的 JdbcTemplate
用法示例:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
public class JdbcTemplateExample {
public static void main(String[] args) {
// 配置数据源
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/yourdatabase");
dataSource.setUsername("yourusername");
dataSource.setPassword("yourpassword");
// 创建 JdbcTemplate 实例
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
// 执行查询
String sql = "SELECT * FROM your_table WHERE id = ?";
Object[] params = new Object[] { 1 };
Map<String, Object> result = jdbcTemplate.queryForMap(sql, params);
// 输出查询结果
System.out.println(result);
// 执行更新
String updateSql = "UPDATE your_table SET name = ? WHERE id = ?";
Object[] updateParams = new Object[] { "new name", 1 };
int rowsAffected = jdbcTemplate.update(updateSql, updateParams);
// 输出更新结果
System.out.println("Rows affected: " + rowsAffected);
}
}
在这个示例中,我们首先配置了一个 DriverManagerDataSource
,然后创建了一个 JdbcTemplate
实例。接着,我们使用 JdbcTemplate
执行了一个查询操作和一个更新操作,并输出了相应的结果。
业务背景
-
日常开发过程中,离不开和数据库的交互。JDBC提供了Java应用程序提供与数据库交互的能力,无论是进行数据检索还是更新。
-
JDBC交互的套路操作有:
-
配置数据库账号、密码、连接信息等参数
-
打开数据库连接
-
根据业务需求编写sql语句
-
预编译并执行sql语句
-
处理执行结果
-
处理抛出的异常
-
处理事务
-
关闭数据库连接
-
-
为了方便Spring框架与数据库的交互,我们需要将上述过程进行抽象化,实现jdbcTemplate模板类, 来执行jdbc交互的大部分通用步骤,形成spring-jdbc模块。spring-jdbc接收参数信息、JdbcTemplate接收业务需求sql进行处理并返回结果,应用返回结果数据。
目标
基于当前实现的 Spring 框架,实现jdbcTempate模板类,完成spring对jdbc调用的封装。
设计
为了方便Spring框架与数据库的交互,我们需要将JDBC交互的过程进行抽象化,实现jdbcTemplate模板类, 来执行jdbc交互的大部分通用步骤。整体设计结构如下图:
-
首先定义好JdbcTemplate类需要支持的操作,包括:解析数据源参数、获取数据源、获取连接、预编译执行sql、处理执行结果、处理异常、关闭连接。
-
通过JdbcTemplate类封装了和JDBC的交互细节,用户只需要通过调用jdbcTemplate方法,就可以很方便的完成数据库交互。
实现
代码结构
源码实现:https://github.com/swg209/spring-study/tree/main/step18-spring-jdbc
类图
- 在整个类图中,通过JdbcAccessor实现InitializingBean,将jdbcTemplate接入当前框架。数据源相关通过DataSourceUtils来接入,JdbcTemplate实现了jdbcOperation定义的接口,实现与数据库的交互操作。
DataSource
用来提供Connection,之所以有那么多辅助类(ConnectionHanlder、ConnectionHandle)是为了可以和数据库事务结合
JdbcTemplate
这个类是进行执行操作的入口类,里面有很多重载方法。
-
T execute(StatementCallback action, boolean closeResources)
这个是JdbcTemplate内部的私有实现方法,JdbcOperations接口中定义的一系列execute()方法也是调用的该方法
JdbcOperations
这个接口定义了非常多的入口方法,实现类就是JdbcTemplate
-
T query(String sql, ResultSetExtractor rse)
-
T query(String sql, ResultSetExtractor rse)
这个是query系列方法的最后调用方法,该方法会调用JdbcTemplate内部的私有execute方法。
ResultSetExtractor
- 进行数据库查询之后会得到结果集即:ResultSet,这个类用于结果集数据提取
RowMapper
- 用于将结果集每行数据转换为需要的类型
实现步骤
-
数据源连接类,
DataSourceUtils
提供Connection。public abstract class DataSourceUtils { public static Connection getConnection(DataSource dataSource) { try { return doGetConnection(dataSource); } catch (SQLException e) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", e); } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { Connection connection = fetchConnection(dataSource); ConnectionHolder holderToUse = new ConnectionHolder(connection); return connection; } private static Connection fetchConnection(DataSource dataSource) throws SQLException { Connection conn = dataSource.getConnection(); if (null == conn) { throw new IllegalArgumentException("DataSource return null from getConnection():" + dataSource); } return conn; } public static void releaseConnection(Connection con, DataSource dataSource) { try { doReleaseConnection(con, dataSource); } catch (SQLException ex) { // logger.debug("Could not close JDBC Connection", ex); } catch (Throwable ex) { // logger.debug("Unexpected exception on closing JDBC Connection", ex); } } public static void doReleaseConnection(Connection con, DataSource dataSource) throws SQLException { if (con == null) { return; } doCloseConnection(con, dataSource); } public static void doCloseConnection(Connection con, DataSource dataSource) throws SQLException { con.close(); } }
涉及连接的多个接口类,包括ConnectionHandler、ConnectionHolder, 以及实现类SimpleConnectionHandler。
public interface ConnectionHandler { Connection getConnection(); default void releaseConnection(Connection con) { } } public class ConnectionHolder { private ConnectionHandler connectionHandler; private Connection currentConnection; public ConnectionHolder(ConnectionHandler connectionHandler) { this.connectionHandler = connectionHandler; } public ConnectionHolder(Connection connection) { this.connectionHandler = new SimpleConnectionHandler(connection); } public ConnectionHandler getConnectionHandler() { return connectionHandler; } protected boolean hasConnection() { return this.connectionHandler != null; } protected void setConnection(Connection connection) { if (null != this.currentConnection) { if (null != this.connectionHandler) { this.connectionHandler.releaseConnection(this.currentConnection); } this.currentConnection = null; } if (null != connection) { this.connectionHandler = new SimpleConnectionHandler(connection); } else { this.connectionHandler = null; } } protected Connection getConnection() { Assert.notNull(this.connectionHandler, "Active connection is required."); if (null == this.currentConnection) { this.currentConnection = this.connectionHandler.getConnection(); } return this.currentConnection; } } public class SimpleConnectionHandler implements ConnectionHandler { private final Connection connection; public SimpleConnectionHandler(Connection connection) { Assert.notNull(connection, "Connection must not be null"); this.connection = connection; } @Override public Connection getConnection() { return this.connection; } }
-
核心类 JdbcTemplate内部完成核心方法
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { private <T> T execute(StatementCallback<T> action, boolean closeResources) { Connection con = DataSourceUtils.getConnection(obtainDataSource()); Statement stmt = null; try { stmt = con.createStatement(); applyStatementSettings(stmt); return action.doInStatement(stmt); } catch (SQLException e) { String sql = getSql(action); JdbcUtils.closeStatement(stmt); stmt = null; throw translateException("ConnectionCallback", sql, e); } finally { if (closeResources) { JdbcUtils.closeStatement(stmt); } } } private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources) { Assert.notNull(psc, "PreparedStatementCreator must not be null"); Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(obtainDataSource()); PreparedStatement ps = null; try { ps = psc.createPreparedStatement(con); applyStatementSettings(ps); T result = action.doInPreparedStatement(ps); return result; } catch (SQLException ex) { String sql = getSql(psc); psc = null; JdbcUtils.closeStatement(ps); ps = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw translateException("PreparedStatementCallback", sql, ex); } finally { if (closeResources) { JdbcUtils.closeStatement(ps); DataSourceUtils.releaseConnection(con, getDataSource()); } } } public <T> T query( PreparedStatementCreator psc, final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) { Assert.notNull(rse, "ResultSetExtractor must not be null"); return execute(psc, new PreparedStatementCallback<T>() { @Override public T doInPreparedStatement(PreparedStatement ps) throws SQLException { ResultSet rs = null; try { if (pss != null) { pss.setValues(ps); } rs = ps.executeQuery(); return rse.extractData(rs); } finally { JdbcUtils.closeResultSet(rs); } } }, true); } ...... }
-
JdbcOperations 数据库交互操作接口.
- StatementCallback 会话回调接口,用于执行任意的JDBC Statement操作
- RowMapper 行处理接口,用于将结果集的每一行映射到Java对象
- ResultSetExtractor 结果集处理接口,用于从整个ResultSet中提取数据。
- PreparedStatementSetter 预编译接口,用于设置PreparedStatement的参数值。
- RowMapperResultSetExtractor 自定义接口或类,用于结合RowMapper和ResultSetExtractor的功能来处理结果集。
- RawMapper -> ColumnRawMapper 、 SingleColumnRawMapper.
RawMapper
:原始映射器接口或类的泛称,用于处理结果集的原始列数据。
ColumnRawMapper
:列原始映射器,用于将结果集的特定列映射到某个类型。
SingleColumnRawMapper
:单列原始映射器,用于将只返回一列数据的查询结果映射到某个类型。
public interface JdbcOperations { <T> T execute(StatementCallback<T> action); void execute(String sql); //--------------------------------------------------------------------- // query //--------------------------------------------------------------------- <T> T query(String sql, ResultSetExtractor<T> res); <T> T query(String sql, Object[] args, ResultSetExtractor<T> rse); <T> List<T> query(String sql, RowMapper<T> rowMapper); <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper); <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse); //--------------------------------------------------------------------- // queryForList //--------------------------------------------------------------------- List<Map<String, Object>> queryForList(String sql); /** * 查询数据库表中某一个字段 */ <T> List<T> queryForList(String sql, Class<T> elementType); <T> List<T> queryForList(String sql, Class<T> elementType, Object... args); List<Map<String, Object>> queryForList(String sql, Object... args); //--------------------------------------------------------------------- // queryForObject //--------------------------------------------------------------------- <T> T queryForObject(String sql, RowMapper<T> rowMapper); <T> T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper); /** * 查询数据库表中 某一条记录的 某一个字段 */ <T> T queryForObject(String sql, Class<T> requiredType); //--------------------------------------------------------------------- // queryForMap //--------------------------------------------------------------------- Map<String, Object> queryForMap(String sql); Map<String, Object> queryForMap(String sql, Object... args); }
测试
事先准备
mysql数据库,配置好连接信息, 建表语句。(也可以后续执行ApiTest#executeSqlTest 完成建表 )
#创建数据库
CREATE DATABASE mybatis;
#创建用户表
USE mybatis;
CREATE TABLE user (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`username` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '用户名',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
属性配置文件
spring.xml
-
配置数据库源连接信息,注册jdbcTemplate bean。
-
我本地的mysql版本是8.0.33,对应的mysql-connector-java也是 8.0.33,留意pom.xml文件,加上该依赖
-
& 是为了转义&符号,不加&allowPublicKeyRetrieval=true,会报java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed 错误,这个异常通常出现在尝试使用JDBC连接到MySQL数据库时,特别是当使用SSL连接到MySQL 8.0或更高版本时。这个异常的原因是JDBC驱动程序默认不允许从服务器检索公钥,这是出于安全考虑,以防止中间人攻击(MITM)。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource"
class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl"
value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="jdbcTemplate"
class="cn.suwg.springframework.jdbc.support.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
测试用例
public class ApiTest {
private JdbcTemplate jdbcTemplate;
@Before
public void init() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
}
@Test
public void executeSqlTest() {
jdbcTemplate.execute("CREATE TABLE user (\n" +
" `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',\n" +
" `username` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '用户名',\n" +
" PRIMARY KEY (id)\n" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';");
}
/**
* 插入数据.
*/
@Test
public void executeInsertSqlTest() {
//插入语句
jdbcTemplate.execute("INSERT INTO user (username) values ('小苏');");
}
@Test
public void queryForListTest() {
List<Map<String, Object>> allResult = jdbcTemplate.queryForList("select * from user");
for (int i = 0; i < allResult.size(); i++) {
System.out.printf("第%d行数据", i + 1);
Map<String, Object> objectMap = allResult.get(i);
System.out.println(objectMap);
}
}
@Test
public void queryListWithColumnClassTypeTest() {
List<String> allResult = jdbcTemplate.queryForList("select username from user", String.class);
for (int i = 0; i < allResult.size(); i++) {
System.out.printf("第%d行数据", i + 1);
String username = allResult.get(i);
System.out.println(username);
}
}
@Test
public void queryListWithColumnClassTypeWithArgTest() {
List<String> allResult = jdbcTemplate.queryForList("select username from user where id=?", String.class, 1);
for (int i = 0; i < allResult.size(); i++) {
System.out.printf("第%d行数据", i + 1);
String username = allResult.get(i);
System.out.println(username);
}
}
@Test
public void queryListWithArgTest() {
List<Map<String, Object>> allResult = jdbcTemplate.queryForList("select * from user where id=?", 1);
for (int i = 0; i < allResult.size(); i++) {
System.out.printf("第%d行数据", i + 1);
Map<String, Object> row = allResult.get(i);
System.out.println(row);
}
}
@Test
public void queryObjectTest() {
String username = jdbcTemplate.queryForObject("select username from user where id=1", String.class);
System.out.println(username);
}
@Test
public void queryMapTest() {
Map<String, Object> row = jdbcTemplate.queryForMap("select * from user where id=1");
System.out.println(row);
}
@Test
public void queryMapWithArgTest() {
Map<String, Object> row = jdbcTemplate.queryForMap("select * from user where id=?", 1);
System.out.println(row);
}
}
测试结果:
-
queryForListTest
-
queryListWithColumnClassTypeTest
-
queryListWithColumnClassTypeWithArgTest
-
queryListWithArgTest
-
queryObjectTest
-
queryMapTest
-
queryMapWithArgTest
- 从测试结果中可以看到,可以正常与数据库进行交互,创建表、插入数据,查询表数据、查询表中某列的数据、根据条件查询表的数据等功能。
总结
- 在本章节中,我们深入探讨了JDBC(Java Database Connectivity)的功能整合,特别是如何通过Spring框架中的
JdbcTemplate
来简化JDBC编程。我们详细分析了JDBC的核心组件、关键特性和用法,并展示了如何通过JdbcTemplate
来执行数据库操作,包括查询、更新处理等。 - 通过本节的学习,我们可以借鉴这些代码的结构和风格,提高自己的编码水平。可以帮助我们后续再遇到查询数据库遇到的问题时,可以更快排查定位问题。
参考书籍:《手写Spring渐进式源码实践》
书籍源代码:https://github.com/fuzhengwei/small-spring
原文地址:https://blog.csdn.net/weixin_37693760/article/details/143651427
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!