🔍 一、为什么必须学 JDBC?------不是"过时",而是"根基"
在 Spring Boot 时代,我们用 `@Mapper`、`JdbcTemplate`、`MyBatis-Plus` 三行代码就完成增删改查。那为什么还要啃 JDBC?
> 💡 答案很朴素:它是所有 Java 数据库框架的底层基石!
> - MyBatis 的 `SqlSession` 是对 `Connection + PreparedStatement` 的封装;
> - Spring 的 `JdbcTemplate` 是对 `PreparedStatement` 和 `ResultSet` 的模板化抽象;
> - 连接池(Druid/Hikari)、事务管理器(`DataSourceTransactionManager`)全基于 JDBC 接口构建。
📌 一句话总结:
> ❗️不学 JDBC,永远只能调 API;学透 JDBC,才能真正看懂框架、写出高性能 SQL、解决事务死锁、排查连接泄漏!
📘 二、JDBC 是什么?------一套标准,不是某个工具!
✅ 官方定义
JDBC(Java Database Connectivity)是由 Sun 公司制定的一套访问关系型数据库的标准接口(API 规范),属于 Java SE 的一部分。
| 角色 | 说明 |
|---|---|
| JDBC(接口) | java.sql.* 包下的 Connection, Statement, ResultSet 等------你写的代码依赖它 |
| JDBC Driver(驱动实现) | MySQL 的 com.mysql.cj.jdbc.Driver、Oracle 的 oracle.jdbc.driver.OracleDriver ------厂商提供,你引入 jar 包 |
> ⚠️ 类比理解:
> JDBC = USB 接口标准(Type-C / USB-A)
> 驱动 = 手机厂商(华为/小米)为自家手机生产的 Type-C 数据线
> ❌ 没有驱动 → 插上 USB 线也传不了数据!
🧩 三、JDBC 核心架构图解(4 大核心接口)
graph LR A[DriverManager] -->|获取| B[Connection] B -->|创建| C[Statement] B -->|预编译| D[PreparedStatement] B -->|调用存储过程| E[CallableStatement] C & D & E -->|执行| F[ResultSet]
| 接口 | 职责 | 关键方法 | 使用场景 |
|---|---|---|---|
DriverManager |
驱动注册中心 & 连接工厂 | getConnection(url, user, pwd) |
加载驱动、获取连接(只用一次) |
Connection |
物理数据库会话 | createStatement(), prepareStatement(), setAutoCommit(false) |
控制事务、创建执行器 |
Statement |
普通 SQL 执行器 | executeQuery(), executeUpdate() |
简单、无参数、一次性 SQL(不推荐生产使用) |
PreparedStatement |
预编译 SQL 执行器 | setString(1,"xxx"), executeQuery() |
✅ 防 SQL 注入、✅ 性能高(缓存执行计划)、✅ 支持批量操作 |
ResultSet |
查询结果游标 | next(), getString("name"), getInt(1) |
遍历、取值、关闭(必须 close!) |
> ✅ 最佳实践口诀:
> "先连库,再预编,设参查,游标遍,最后关!"
> (Connection → PreparedStatement → setXxx → executeQuery → next + getXxx → close)
🛠 四、JDBC 操作六步法(含完整代码)
> ✅ 务必按顺序执行,资源关闭顺序必须「反向」!
> `ResultSet` → `Statement` → `Connection`(否则可能报 `Closed Connection` 异常)
✅ Step 1:加载驱动(MySQL 8+)
// ✅ MySQL 8.x 推荐写法(自动注册,可省略) Class.forName("com.mysql.cj.jdbc.Driver"); // ⚠️ MySQL 5.x 写法(已过时,仅兼容) // Class.forName("com.mysql.jdbc.Driver");
✅ Step 2:获取连接(URL 参数很重要!)
String url = "jdbc:mysql://localhost:3306/jdbc_db?" + "useSSL=false&" + // 关闭 SSL(本地开发) "serverTimezone=Asia/Shanghai&" + // 时区(避免时间错乱) "characterEncoding=utf8"; // 字符集(防中文乱码) Connection conn = DriverManager.getConnection(url, "root", "1111");
✅ Step 3:创建 PreparedStatement(强烈推荐!)
String sql = "INSERT INTO student (id, name, age, sex, address) VALUES (?, ?, ?, ?, ?)"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, 101); pstmt.setString(2, "张三"); pstmt.setInt(3, 25); pstmt.setString(4, "男"); pstmt.setString(5, "北京市朝阳区");
✅ Step 4:执行操作
| 操作类型 | 方法 | 返回值 | 示例 |
|---|---|---|---|
| 查询(SELECT) | pstmt.executeQuery() |
ResultSet |
rs = pstmt.executeQuery(); |
| 增/删/改(DML) | pstmt.executeUpdate() |
int(影响行数) |
int rows = pstmt.executeUpdate(); |
✅ Step 5:处理 ResultSet(注意游标初始位置!)
ResultSet rs = pstmt.executeQuery(); // ⚠️ 游标初始在第 0 行(第一行前),必须 next() 才能取值! while (rs.next()) { int id = rs.getInt("id"); // ✅ 推荐:用列名(更可读) String name = rs.getString("name"); // 或用索引(从 1 开始!)→ rs.getString(2) }
✅ Step 6:关闭资源(必须 try-finally 或 try-with-resources!)
finally { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); if (conn != null) conn.close(); // 归还连接池(若使用) }
> 💡 进阶技巧:使用 try-with-resources(JDK 7+)自动关闭
try (Connection conn = DBUtils.getConn(); PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { /* 处理结果 */ } } catch (SQLException e) { e.printStackTrace(); } // 自动 close()!
🚫 五、致命风险:SQL 注入攻击与防御(必考!)
❌ 危险写法(字符串拼接)→ 账户密码全裸奔!
// 用户输入:username = "admin' -- ",password = "123" String sql = "SELECT * FROM sys_user WHERE username='" + username + "' AND password='" + password + "'"; // 实际执行: // SELECT * FROM sys_user WHERE username='admin' -- ' AND password='123' // → 注释掉密码校验!直接登录成功!
✅ 正确方案:PreparedStatement 占位符(唯一可靠方案!)
String sql = "SELECT * FROM sys_user WHERE username = ? AND password = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // ✅ 参数化,数据库当「值」而非「SQL 片段」 pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery();
> ✅ 原理:PreparedStatement 将 SQL 与参数分离,数据库先编译 SQL 模板,再安全绑定参数值,彻底杜绝注入!
💰 六、事务控制:转账案例(ACID 四大特性落地)
📌 需求
A 向 B 转账 1000 元,要求:
✅ A 扣款成功 & B 加款成功 → 全部提交
❌ A 扣款成功 & B 加款失败 → 全部回滚(钱不能丢!)
✅ 核心代码(手动事务)
Connection conn = null; try { conn = DBUtils.getConn(); conn.setAutoCommit(false); // 🔑 关闭自动提交,开启事务 String sql1 = "UPDATE account SET amount = amount - 1000 WHERE aid = 1"; String sql2 = "UPDATE account SET amount = amount + 1000 WHERE aid = 2"; PreparedStatement p1 = conn.prepareStatement(sql1); PreparedStatement p2 = conn.prepareStatement(sql2); p1.executeUpdate(); // 扣款 // int i = 1 / 0; // ✅ 模拟异常:此处抛异常则触发回滚 p2.executeUpdate(); // 加款 conn.commit(); // ✅ 全部成功,提交事务 System.out.println("转账成功!"); } catch (Exception e) { e.printStackTrace(); try { if (conn != null) conn.rollback(); // 🔑 回滚! System.out.println("转账失败,已回滚!"); } catch (SQLException ex) { ex.printStackTrace(); } } finally { DBUtils.close(conn); }
✅ ACID 特性对应实现:
| 特性 | JDBC 实现方式 |
|---|---|
| 原子性(Atomicity) | commit() / rollback() 控制整体成败 |
| 一致性(Consistency) | 事务内约束(如外键、唯一键)自动校验 |
| 隔离性(Isolation) | conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) |
| 持久性(Durability) | 提交后数据写入磁盘(数据库保证) |
🐬 七、性能瓶颈:为什么需要数据库连接池?
❌ 传统模式痛点
每次请求 → 创建 Connection(TCP 握手 + 认证)→ 执行 SQL → close()(四次挥手) → 高并发下:连接创建/销毁耗时 > SQL 执行耗时 → 系统雪崩!
✅ 连接池原理(复用连接)
graph LR A[应用] -->|借| B[连接池] B --> C[空闲连接1] B --> D[空闲连接2] B --> E[空闲连接3] C & D & E -->|归还| B
| 方案 | 特点 | 现状 |
|---|---|---|
| c3p0 / DBCP | 老牌开源池,配置复杂 | ❌ 已基本淘汰 |
| Druid(阿里) | 监控强大(Web 控制台)、中文文档好、Spring Boot 默认集成 | ✅ 国内主流选择 |
| HikariCP(日本) | 性能最强(号称"光速")、Spring Boot 2.0+ 官方默认 | ✅ 国际主流 & 推荐首选 |
🧱 八、企业级封装:BaseDAO ------ 手写你的第一个"轻量 ORM"
> 💡 目标:用一个 `BaseDao<T>`,支持任意实体类的通用增删改查 + 分页,零 SQL 硬编码!
✅ Step 1:定义通用分页对象 `PageInfo<T>`
public class PageInfo<T> { private List<T> data; // 当前页数据 private Long total; // 总条数 private Integer pageNum; // 当前页码 private Integer pageSize;// 每页条数 // getter/setter... }
✅ Step 2:BaseDao 核心封装(反射 + 泛型 + PreparedStatement)
public class BaseDao { // ✅ 通用查询列表(支持 ? 占位符) public <T> List<T> selectList(String sql, Class<T> cls, Object... params) { List<T> list = new ArrayList<>(); try (Connection conn = DBUtils.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { // 设置参数 for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { ResultSetMetaData meta = rs.getMetaData(); int columnCount = meta.getColumnCount(); while (rs.next()) { T obj = cls.getDeclaredConstructor().newInstance(); for (int i = 1; i <= columnCount; i++) { String colName = meta.getColumnLabel(i); // 获取列别名(如 createTime) Object value = rs.getObject(colName); Field field = cls.getDeclaredField(colName); field.setAccessible(true); field.set(obj, value); } list.add(obj); } } } catch (Exception e) { e.printStackTrace(); } return list; } // ✅ 通用分页查询(自动计算 count + limit) public <T> PageInfo<T> selectPage(String sql, Class<T> cls, int pageNum, int pageSize) { PageInfo<T> page = new PageInfo<>(); // 1. 查询当前页数据 String pageSql = sql + " LIMIT " + (pageNum - 1) * pageSize + "," + pageSize; List<T> dataList = selectList(pageSql, cls); // 2. 查询总条数(子查询包装) String countSql = "SELECT COUNT(*) FROM (" + sql + ") AS tmp"; long total = 0; try (Connection conn = DBUtils.getConn(); PreparedStatement ps = conn.prepareStatement(countSql); ResultSet rs = ps.executeQuery()) { if (rs.next()) total = rs.getLong(1); } catch (Exception e) { e.printStackTrace(); } page.setData(dataList); page.setTotal(total); page.setPageNum(pageNum); page.setPageSize(pageSize); return page; } // ✅ 通用更新(INSERT/UPDATE/DELETE) public boolean update(String sql, Object... params) { try (Connection conn = DBUtils.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } return ps.executeUpdate() > 0; } catch (Exception e) { e.printStackTrace(); return false; } } }
✅ Step 3:业务 DAO 继承使用(极简!)
public class UserDao extends BaseDao { // ✅ 不用写 SQL!复用父类方法 public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM sys_user WHERE username = ? AND password = ?"; return selectOne(sql, User.class, username, password); } public void updateUserState(Long id, Integer deleted) { String sql = "UPDATE sys_user SET deleted = ?, deleted_time = NOW() WHERE id = ?"; update(sql, deleted, id); } public PageInfo<User> findPage(int pageNum, int pageSize) { String sql = "SELECT * FROM sys_user WHERE deleted = 0"; return selectPage(sql, User.class, pageNum, pageSize); } }
> ✅ 优势总结:
> - ✅ 零 SQL 硬编码(SQL 写在方法里,非 XML)
> - ✅ 强类型安全(泛型 `<T>` 编译期检查)
> - ✅ 自动映射(字段名 ↔ 列名一致即可)
> - ✅ 开箱即用分页(count + limit 一键搞定)
> - ✅ 为后续接入 MyBatis 奠定认知基础
🌈 九、总结:JDBC 学习路线图(附学习建议)
| 阶段 | 掌握目标 | 推荐练习 |
|---|---|---|
| 入门 | 能手写 6 步完成 CRUD、理解 PreparedStatement 防注入 | 学生管理系统增删改查 |
| 进阶 | 掌握事务控制、连接池配置(Druid/Hikari)、批处理 addBatch() |
银行转账 + 并发压力测试 |
| 高手 | 封装 BaseDAO、理解 JDBC 四大接口 |