22.JdbcTemplate简化JDBC操作
1.核心作用:消除样板代码 (Boilerplate Code)
如果你直接写原生 JDBC,哪怕只是查一个 ID,你都需要写一大堆代码:获取连接、创建 Statement、处理 ResultSet、捕获异常,最后还要在 finally 块里小心翼翼地关闭连接。
JdbcTemplate 帮你自动完成了以下工作:
开启连接 (Connection)
创建语句 (Statement)
关闭资源 (Resultset, Statement, Connection) —— 这一点至关重要,防止数据库连接泄露。
处理异常 (将难用的 SQLException 转换为更通用的 Spring DataAccessException)
2.直观对比:原生 JDBC vs JdbcTemplate
-
🔴使用原生 JDBC (繁琐)
public User getUser(int id) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = dataSource.getConnection(); // 1. 获取连接 String sql = "SELECT * FROM users WHERE id = ?"; ps = conn.prepareStatement(sql); // 2. 创建语句 ps.setInt(1, id); rs = ps.executeQuery(); // 3. 执行 if (rs.next()) { User user = new User(); user.setName(rs.getString("name")); // 4. 映射数据 return user; } } catch (SQLException e) { e.printStackTrace(); // 5. 处理异常 } finally { // 6. 痛苦的关闭资源环节 try { if (rs != null) rs.close(); } catch (SQLException e) {} try { if (ps != null) ps.close(); } catch (SQLException e) {} try { if (conn != null) conn.close(); } catch (SQLException e) {} } return null; }-
rs.close()关闭的是什么?- 要明白“关闭的是什么”,我们需要把视角从 Java 代码 移到 数据库服务端 和 内存 两个地方来看。
1. 数据库游标 (Cursor) 耗尽 —— 最直接的原因 什么是 ResultSet? ResultSet 不仅仅是内存里的数据,它通常对应数据库服务端的一个打开的游标 (Cursor)。 资源限制: 数据库对单个连接(Connection)能同时打开的游标数量是有严格限制的(比如 Oracle 默认可能限制 300 个)。 后果: 如果你在一个长连接中(或者在一个复杂的业务逻辑里),反复执行查询而不关闭 ResultSet,游标就会越积越多。很快你就会遇到类似 ORA-01000: maximum open cursors exceeded 的错误,导致程序崩溃。即使 Connection 还没断,你也查不了数据了。 2. 连接池 (Connection Pool) 的存在 —— 最现实的原因 在实际的企业级开发(包括 Spring 的 JdbcTemplate)中,Connection 是不关闭的。 复用机制: 当你调用 conn.close() 时,实际上并没有断开与数据库的 TCP 连接,而是把这个 Connection “归还” 给了连接池,供下一个线程使用。 泄露风险: 如果你没有关闭 ResultSet 和 Statement 就归还了连接,这些旧的资源对象可能仍然挂在这个 Connection 上。这会导致严重的内存泄漏和数据库资源占用,最终拖垮整个数据库连接池。- 技术层面的解释:到底释放了什么?
A. 在数据库服务端:销毁“游标 (Cursor)” 这是最关键的。 • 当你查询数据时,数据库并不是把 100 万行数据一次性全扔给 Java,而是在数据库内存里建立了一个“游标” (Cursor),指向查询结果的第一行。 • 数据库会为这个游标分配一块内存区。 • rs.close() 的作用:Java 发送一个网络信号给数据库,说:“嘿,这个查询我看完了。” 数据库收到信号后,立即销毁这个游标,回收这块内存。 • 如果不关:数据库那边会以为你还没看完,一直帮你留着这块内存。连接复用次数多了,数据库内存就被撑爆了。 B. 在 Java 客户端:清空“行缓存 (Row Buffer)” • ResultSet 对象本身在 Java 的堆内存里也占地方。它内部往往会有缓存(比如一次预读取 50 行数据)。 • rs.close() 的作用:切断 Java 对象与数据库的联系,并清空这些缓存数据,让 Java 的垃圾回收器(GC)能尽快回收这部分内存。- rs.next()是不是相当于数据库游标移向下一项,rs.close()相当于告诉数据库可以回收当前的游标呢?
1. 第一次 rs.next() 时: Java 驱动会跟数据库说:“我要读数据了。” 数据库不是只给一行,而是根据 Fetch Size(默认通常是 10行、50行或更多,取决于驱动),一次性打包一批数据(比如 10 行)通过网络发给 Java。 Java 把这 10 行数据存此时的 本地内存(Row Buffer) 里。 2.执行 rs.getXXX() 时: Java 直接从本地内存里把刚才存好的数据拿出来给你。完全不需要联网,速度极快(纳秒级)。 3. 接下来的 9 次 rs.next(): 驱动发现:“哎?内存里还有刚才那批没读完的数据呢。” 于是它只是在内存里把指针下移一行。也不需要联网。 4. 第 11 次 rs.next() 时: 驱动发现:“内存里的 10 行读完了,空了。” 此时再次联网,去数据库再打包抓取下 10 行回来。-
数据库送来的行数据到底在本地是怎样存储的呀
它绝不是以你想象的 Integer、String、Date 这种 Java 对象的形式存储的。
在大多数主流数据库驱动(比如 MySQL Connector/J)的默认模式下,本地缓存里的行数据,是以 紧凑的字节数组 (byte arrays) 形式存储的。✅ 真实的存储方式(MySQL 驱动为例): 它通常是一个 二维字节数组 或者类似的紧凑结构。
// ✅ 真实的内存样子(伪代码) byte[][] rowData = { {0x31}, // 对应 "1" 的 ASCII 码 {0x54, 0x6F, 0x6D}, // 对应 "Tom" 的 ASCII 码 {0x32, 0x30} // 对应 "20" 的 ASCII 码 };id咋就知道要返回第一列对应的字节数组呢
既然数据只是一堆没有任何标记的“字节”,驱动怎么知道哪一段字节属于 id,哪一段属于 name呢?
答案是:靠“元数据” (Metadata) 这一张藏宝图。
- 建立映射:收到快递前的“发货清单”
当你执行 executeQuery("select id, name from users") 时,数据库返回给 Java 驱动的数据流其实是分两段的:
第一段:元数据 (Metadata) —— 也就是“列定义”
数据库先告诉驱动:“老兄,接下来的数据里,第 1 列叫 id,是整数;第 2 列叫 name,是字符串。”
Java 驱动收到这个信息后,会在内部悄悄建立一个 HashMap(或者类似的查找表):
// 驱动内部的逻辑映射表 (Column Name -> Index) Map<String, Integer> columnMap = new HashMap<>(); columnMap.put("id", 1); columnMap.put("name", 2);- 查找取值:当你调用 rs.getInt("id") 时
好了,现在内存里既有 “映射表”,又有 “字节数据”。当你写下代码 rs.getInt("id") 时,驱动内部实际上走了这么几步棋:
第一步:查字典 (Name -> Index)
驱动首先拿到你传入的字符串 "id",去那张 HashMap 里查:
“id 对应第几列呀?” Map 回答:“对应 第 1 列。”
第二步:定位数据 (Index -> Bytes)
驱动拿着 Index = 1,去当前的行缓存(那个二维数组)里找:
“把 第 1 个 数组槽里的字节拿给我。” 数组回答:“给你,这是 {0x31}。”
- 此时java内存里面的这个指针和数据库那个指针指向的行不同步:
1. 为什么会不同步?(为了性能) 我们设定 Fetch Size = 100(一次抓取 100 行),来看看具体发生了什么: 第一阶段:刚开始 (rs.next() 第一次) • Java 端:发请求说“我要数据”。 • 数据库端:立刻扫描了 1 到 100 行,把这 100 行打包扔进网络,然后停在第 100 行的位置等着。 • Java 端:收到了包,把指针指向 第 1 行,开始处理。 此时的状态差距: • Java 指针:在 第 1 行。 • 数据库指针:已经在 第 100 行。 (数据库超前了整整 99 行!) 第二阶段:处理中 (rs.next() 第 50 次) • Java 端:你还在慢悠悠地处理第 50 行的数据 (rs.getInt...)。 • 数据库端:它在摸鱼。因为它上次一口气干完了 100 行的活,现在它维持在第 100 行的位置,等着你把手里的活干完再来找它。 此时的状态差距: • Java 指针:在 第 50 行。 • 数据库指针:依然在 第 100 行。 第三阶段:再次进货 (rs.next() 第 101 次) • Java 端:处理完第 100 行了,一翻页,发现本地缓存空了。于是发信号给数据库:“再来点!” • 数据库端:立刻从第 100 行继续往下扫,扫描 101 到 200 行,打包发送,然后停在 第 200 行。但是指针不同步会产生读到脏数据,提前50行已经到java内存,但是如果别的程序更新了数据库前50行的内容呢,这不就导致了现在的Java程序还在使用之前的旧数据吗。
-
ps.close()关闭的是什么?
ps.close()的核心目的是:销毁“编译好的执行计划”。
1. 服务端:最核心的原因 —— “销毁模板” 这是 PreparedStatement 相比普通 Statement 最特殊的地方。 A. 什么是 PreparedStatement? 当你执行 conn.prepareStatement("select * from users where id = ?") 时,数据库并不是简单地记录这行字符串,而是做了大量繁重的工作: 1. 语法分析 (Parsing):检查 SQL 语法对不对。 2. 语义分析:检查 users 表存不存在,字段对不对。 3. 制定执行计划 (Execution Plan):数据库会思考“我是走索引查找快,还是全表扫描快?”。这个过程非常消耗 CPU。 4. 缓存模板:数据库把这个**“最佳路径”**存起来,分配一个 Statement ID (句柄) 给 Java。 B. 如果不关闭 (ps.close()) 会怎样? 即使你的 Java 代码跑完了,如果你不发指令说“我用完了”,数据库服务端会一直保留着这个 Statement ID 和对应的 执行计划。 • 内存泄露:数据库服务器的内存里堆积了成千上万个不再使用的“执行计划”。 • 句柄耗尽:数据库也有 max_prepared_stmt_count 限制(比如 MySQL 默认可能是 16382)。一旦超过这个数,整个数据库都会报错,谁也别想再预编译 SQL 了。- 能不能只ps.close()而不rs.close()呢
答案是可以,根据 JDBC 规范,当一个 Statement (或 PreparedStatement) 被关闭时,Statement 对象是 ResultSet 的“持有者”,它会自动把当前由它生成的所有 ResultSet 全部关闭。接下来要是在来一个rs.close(),那便是多余了。
-
-
🟢 使用 JdbcTemplate (清爽)
DataSource (数据源),这是 JdbcTemplate 的“插头”。它不负责管理连接,它只负责找 DataSource 要连接。通常这里连接的是连接池(如 HikariCP)。
@Autowired private JdbcTemplate jdbcTemplate; public User getUser(int id) { String sql = "SELECT * FROM users WHERE id = ?"; // 只需要这一行,资源管理、异常处理全自动搞定 return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id); }-
最重要的就是结果集映射啦
(1).手动映射---效率高(底层不是使用反射)
你需要一个个去取值,还要处理驼峰和下划线的对应关系。
String sql = "select id, name from users where id = ?"; // 一行搞定 User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { User u = new User(); u.setId(rs.getInt("id")); u.setName(rs.getString("name")); return u; }, 1); // 最后的 1 是参数(2).自动挡---懒鬼行为(底层使用反射)
String sql = "SELECT id, user_name, age FROM users"; // 只需要这一行,完全不需要手动 get/set List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));自动挡是怎么做到的?(命名规则)
BeanPropertyRowMapper 很聪明,它默认支持 “下划线转驼峰” 的规则:
数据库列名:user_name (或者 USERNAME)
Java 属性名:userName
只要名字能对应上(忽略大小写和下划线),它就能自动通过反射调用 setUserName() 方法把值塞进去。注意:自动档映射对象名字和数据库字段名字一定要对应上,一模一样最好,不然会出现映射为null问题。
总结
日常开发 / 数据量小 / 字段多且对齐: 👉 用 BeanPropertyRowMapper(省时省力,代码清爽)。追求极致性能 / 数据量巨大 / 字段名瞎起: 👉 自己写 RowMapper(完全掌控,速度最快)。
-
3.JdbcTemplate最常见的用法
| 分类 | 方法名 | 返回值 | 用途描述 | 常用代码范例 (简写) |
|---|---|---|---|---|
| 增 / 删 / 改 | update |
int |
执行 INSERT, UPDATE, DELETE。 返回受影响的行数。 |
int rows = jdbcTemplate.update("DELETE FROM user WHERE id=?", 1); |
| 查 (单行对象) | queryForObject |
T (实体对象) |
查询一行记录并自动封装成 Java 对象。 注意:查不到或查出多条会报错。 |
User u = jdbcTemplate.queryForObject("SELECT * FROM user WHERE id=?", new BeanPropertyRowMapper<>(User.class), 1); |
| 查 (单个值) | queryForObject |
Class<T> |
查询一行一列 (如 count, name, id)。 用于统计或查单字段。 |
Integer count = jdbcTemplate.queryForObject("SELECT count(*) FROM user", Integer.class); |
| 查 (多行列表) | query |
List<T> |
查询多行记录并封装成对象列表。 查不到返回空 List (不会报错)。 |
List<User> list = jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class)); |
| 查 (原始 Map) | queryForMap |
Map<String, Object> |
查一行,返回 Key=列名, Value=列对应的值的 。 | Map<String, Object> map = jdbcTemplate.queryForMap("SELECT * FROM user WHERE id=?", 1); |
| 查 (原始 List) | queryForList |
List<Map<String, Object>> |
查多行,返回 Map 的列表。 适合报表或动态列数据。 |
List<Map<String, Object>> list = jdbcTemplate.queryForList("SELECT * FROM user"); |
| 高级 (回填 ID) | update(带KeyHolder) |
int |
插入数据并获取数据库生成的自增 ID。 需配合 KeyHolder 使用。 |
jdbcTemplate.update(psc, keyHolder);*(需实现 PreparedStatementCreator)* |
| 高级 (批量) | batchUpdate |
int[] |
批量执行增删改。 适合一次性插入几千条数据。 |
jdbcTemplate.batchUpdate("INSERT...", new BatchPreparedStatementSetter() {...}); |
| DDL 操作 | execute |
void |
执行建表、删表、清空表 (TRUNCATE) 等指令。 无返回值。 |
jdbcTemplate.execute("CREATE TABLE log (msg varchar(100))"); |

浙公网安备 33010602011771号