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) 这一张藏宝图。

      1. 建立映射:收到快递前的“发货清单”
        当你执行 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);
      
      1. 查找取值:当你调用 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))");
posted @ 2025-12-14 16:45  那就改变世界吧  阅读(4)  评论(0)    收藏  举报